Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-26 15:07:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-26 15:07:29 +0300
commit597d5ed08988cb00681eaf252d04ebae4bd24731 (patch)
treefa6c90ecda00858be51b790dad9e4d9098d29fdb
parente2cf652edb5e9d9fa9a081952070074c07bf651e (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml13
-rw-r--r--.rubocop_todo/layout/line_end_string_concatenation_indentation.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml2
-rw-r--r--.rubocop_todo/rspec/before_all.yml64
-rw-r--r--.rubocop_todo/style/percent_literal_delimiters.yml1
-rw-r--r--.rubocop_todo/style/redundant_freeze.yml1
-rw-r--r--.rubocop_todo/style/redundant_regexp_escape.yml1
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js2
-rw-r--r--app/assets/javascripts/service_desk/components/service_desk_list_app.vue17
-rw-r--r--app/assets/javascripts/service_desk/index.js2
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue15
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss13
-rw-r--r--app/assets/stylesheets/pages/notes.scss1
-rw-r--r--app/assets/stylesheets/pages/settings.scss4
-rw-r--r--app/controllers/concerns/integrations/params.rb10
-rw-r--r--app/controllers/graphql_controller.rb2
-rw-r--r--app/helpers/emails_helper.rb39
-rw-r--r--app/mailers/emails/members.rb16
-rw-r--r--app/mailers/previews/notify_preview.rb7
-rw-r--r--app/models/integrations/base_chat_notification.rb13
-rw-r--r--app/models/integrations/discord.rb19
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/ml/model_version.rb2
-rw-r--r--app/serializers/integrations/event_entity.rb5
-rw-r--r--app/services/members/update_service.rb1
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/views/admin/users/_access_levels.html.haml96
-rw-r--r--app/views/admin/users/_admin_notes.html.haml16
-rw-r--r--app/views/admin/users/_form.html.haml112
-rw-r--r--app/views/devise/sessions/_new_base.html.haml3
-rw-r--r--app/views/notify/member_about_to_expire_email.html.haml6
-rw-r--r--app/views/notify/member_about_to_expire_email.text.erb5
-rw-r--r--app/workers/all_queues.yml18
-rw-r--r--app/workers/members/expiring_email_notification_worker.rb28
-rw-r--r--app/workers/members/expiring_worker.rb32
-rw-r--r--config/feature_flags/development/member_expiring_email_notification.yml (renamed from config/feature_flags/development/cache_introspection_query.yml)10
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20230707003301_add_expiry_notified_at_to_member.rb20
-rw-r--r--db/post_migrate/20230714015909_add_index_for_member_expiring_query.rb18
-rw-r--r--db/schema_migrations/202307070033011
-rw-r--r--db/schema_migrations/202307140159091
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/integrations.md11
-rw-r--r--doc/ci/examples/authenticating-with-hashicorp-vault/index.md12
-rw-r--r--doc/development/secure_coding_guidelines.md61
-rw-r--r--doc/integration/mattermost/index.md77
-rw-r--r--doc/integration/vault.md2
-rw-r--r--doc/user/project/integrations/discord_notifications.md8
-rw-r--r--doc/user/project/ml/experiment_tracking/mlflow_client.md48
-rw-r--r--lib/api/helpers/integrations_helpers.rb3
-rw-r--r--lib/api/ml_model_packages.rb2
-rw-r--r--lib/gitlab/regex.rb318
-rw-r--r--lib/gitlab/regex/bulk_imports.rb40
-rw-r--r--lib/gitlab/regex/merge_requests.rb15
-rw-r--r--lib/gitlab/regex/packages.rb273
-rw-r--r--locale/gitlab.pot26
-rw-r--r--qa/gdk/gdk.yml1
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb7
-rw-r--r--rubocop/cop/rspec/before_all.rb46
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb20
-rw-r--r--spec/factories/integrations.rb7
-rw-r--r--spec/factories/ml/model_versions.rb2
-rw-r--r--spec/frontend/service_desk/components/service_desk_list_app_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js40
-rw-r--r--spec/helpers/ci/variables_helper_spec.rb62
-rw-r--r--spec/lib/gitlab/regex_spec.rb1
-rw-r--r--spec/mailers/notify_spec.rb62
-rw-r--r--spec/migrations/20230714015909_add_index_for_member_expiring_query_spec.rb16
-rw-r--r--spec/migrations/add_expiry_notified_at_to_member_spec.rb21
-rw-r--r--spec/models/integrations/discord_spec.rb22
-rw-r--r--spec/models/member_spec.rb13
-rw-r--r--spec/models/ml/model_version_spec.rb6
-rw-r--r--spec/requests/api/ci/jobs_spec.rb2
-rw-r--r--spec/rubocop/cop/rspec/before_all_spec.rb74
-rw-r--r--spec/serializers/integrations/event_entity_spec.rb21
-rw-r--r--spec/services/members/update_service_spec.rb37
-rw-r--r--spec/services/ml/find_or_create_model_version_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb21
-rw-r--r--spec/workers/members/expiring_email_notification_worker_spec.rb50
-rw-r--r--spec/workers/members/expiring_worker_spec.rb27
87 files changed, 1570 insertions, 557 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index d52c1784aae..85c9b74b6a6 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -568,6 +568,19 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/routes/directs/*.rb'
- 'ee/spec/lib/sidebars/**/*.rb'
+RSpec/BeforeAll:
+ Enabled: true
+ Include:
+ - 'spec/**/*.rb'
+ - 'ee/spec/**/*.rb'
+ # Conflict with RSpec/AvoidTestProf
+ Exclude:
+ - 'spec/migrations/**/*.rb'
+ - 'ee/spec/migrations/**/*.rb'
+ - 'spec/lib/gitlab/background_migration/**/*.rb'
+ - 'ee/spec/lib/gitlab/background_migration/**/*.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb'
+
RSpec/FactoryBot/StrategyInCallback:
Enabled: true
Include:
diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
index 62bf63d1bb9..53954249ed4 100644
--- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
+++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
@@ -187,6 +187,7 @@ Layout/LineEndStringConcatenationIndentation:
- 'lib/gitlab/path_regex.rb'
- 'lib/gitlab/reference_counter.rb'
- 'lib/gitlab/regex.rb'
+ - 'lib/gitlab/regex/bulk_imports.rb'
- 'lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb'
- 'lib/gitlab/slash_commands/presenters/run.rb'
- 'lib/gitlab/tracking/standard_context.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index f483c9ac5f2..797ae5068af 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -2821,6 +2821,8 @@ Layout/LineLength:
- 'lib/gitlab/quick_actions/merge_request_actions.rb'
- 'lib/gitlab/rack_attack.rb'
- 'lib/gitlab/regex.rb'
+ - 'lib/gitlab/regex/bulk_imports.rb'
+ - 'lib/gitlab/regex/packages.rb'
- 'lib/gitlab/relative_positioning/item_context.rb'
- 'lib/gitlab/repository_size_error_message.rb'
- 'lib/gitlab/sample_data_template.rb'
diff --git a/.rubocop_todo/rspec/before_all.yml b/.rubocop_todo/rspec/before_all.yml
new file mode 100644
index 00000000000..faffd1d1119
--- /dev/null
+++ b/.rubocop_todo/rspec/before_all.yml
@@ -0,0 +1,64 @@
+---
+# Cop supports --autocorrect.
+RSpec/BeforeAll:
+ Details: grace period
+ Exclude:
+ - 'spec/finders/packages/go/version_finder_spec.rb'
+ - 'spec/helpers/users_helper_spec.rb'
+ - 'spec/lib/backup/database_spec.rb'
+ - 'spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb'
+ - 'spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb'
+ - 'spec/lib/gitlab/ci/components/instance_path_spec.rb'
+ - 'spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb'
+ - 'spec/lib/gitlab/data_builder/deployment_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing_spec.rb'
+ - 'spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/tables_locker_spec.rb'
+ - 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
+ - 'spec/lib/gitlab/graphql/pagination/connections_spec.rb'
+ - 'spec/lib/gitlab/http_spec.rb'
+ - 'spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb'
+ - 'spec/lib/unnested_in_filters/rewriter_spec.rb'
+ - 'spec/migrations/add_namespaces_emails_enabled_column_data_spec.rb'
+ - 'spec/migrations/add_projects_emails_enabled_column_data_spec.rb'
+ - 'spec/models/ci/commit_with_pipeline_spec.rb'
+ - 'spec/models/concerns/bulk_insert_safe_spec.rb'
+ - 'spec/models/concerns/bulk_insertable_associations_spec.rb'
+ - 'spec/models/concerns/token_authenticatable_spec.rb'
+ - 'spec/models/milestone_spec.rb'
+ - 'spec/models/postgresql/replication_slot_spec.rb'
+ - 'spec/rack_servers/puma_spec.rb'
+ - 'spec/requests/api/composer_packages_spec.rb'
+ - 'spec/requests/api/graphql/mutations/work_items/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/work_items/update_spec.rb'
+ - 'spec/requests/api/users_spec.rb'
+ - 'spec/scripts/lib/glfm/update_example_snapshots_spec.rb'
+ - 'spec/services/bulk_imports/relation_batch_export_service_spec.rb'
+ - 'spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb'
+ - 'spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb'
+ - 'spec/services/packages/composer/create_package_service_spec.rb'
+ - 'spec/services/packages/go/create_package_service_spec.rb'
+ - 'spec/services/work_items/widgets/hierarchy_service/create_service_spec.rb'
+ - 'spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb'
+ - 'spec/support_specs/helpers/stub_feature_flags_spec.rb'
+ - 'spec/tasks/gitlab/backup_rake_spec.rb'
+ - 'spec/tasks/gitlab/ci_secure_files/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/container_registry_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/decomposition/connection_status_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/lock_writes_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/migration_fix_15_11_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/validate_config_rake_spec.rb'
+ - 'spec/tasks/gitlab/db_rake_spec.rb'
+ - 'spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/gitaly_rake_spec.rb'
+ - 'spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/packages/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/snippets_rake_spec.rb'
+ - 'spec/tasks/gitlab/terraform/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/workhorse_rake_spec.rb'
+ - 'spec/tasks/gitlab/x509/update_rake_spec.rb'
+ - 'spec/tasks/migrate/schema_check_rake_spec.rb'
+ - 'spec/workers/loose_foreign_keys/cleanup_worker_spec.rb'
diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml
index bb29652fe96..e6eb7f2a47a 100644
--- a/.rubocop_todo/style/percent_literal_delimiters.yml
+++ b/.rubocop_todo/style/percent_literal_delimiters.yml
@@ -505,6 +505,7 @@ Style/PercentLiteralDelimiters:
- 'lib/gitlab/query_limiting/transaction.rb'
- 'lib/gitlab/reference_extractor.rb'
- 'lib/gitlab/regex.rb'
+ - 'lib/gitlab/regex/bulk_imports.rb'
- 'lib/gitlab/sanitizers/exception_message.rb'
- 'lib/gitlab/sanitizers/exif.rb'
- 'lib/gitlab/search/abuse_detection.rb'
diff --git a/.rubocop_todo/style/redundant_freeze.yml b/.rubocop_todo/style/redundant_freeze.yml
index 0877c752827..b524d03d94f 100644
--- a/.rubocop_todo/style/redundant_freeze.yml
+++ b/.rubocop_todo/style/redundant_freeze.yml
@@ -187,6 +187,7 @@ Style/RedundantFreeze:
- 'lib/gitlab/rack_attack/request.rb'
- 'lib/gitlab/redis/hll.rb'
- 'lib/gitlab/regex.rb'
+ - 'lib/gitlab/regex/packages.rb'
- 'lib/gitlab/robots_txt/parser.rb'
- 'lib/gitlab/saas.rb'
- 'lib/gitlab/sanitizers/exception_message.rb'
diff --git a/.rubocop_todo/style/redundant_regexp_escape.yml b/.rubocop_todo/style/redundant_regexp_escape.yml
index b1ab72bac61..7d10d3e20b3 100644
--- a/.rubocop_todo/style/redundant_regexp_escape.yml
+++ b/.rubocop_todo/style/redundant_regexp_escape.yml
@@ -70,6 +70,7 @@ Style/RedundantRegexpEscape:
- 'lib/gitlab/quick_actions/extractor.rb'
- 'lib/gitlab/quick_actions/timeline_text_and_date_time_separator.rb'
- 'lib/gitlab/regex.rb'
+ - 'lib/gitlab/regex/packages.rb'
- 'lib/gitlab/search/abuse_detection.rb'
- 'lib/gitlab/task_helpers.rb'
- 'lib/gitlab/url_sanitizer.rb'
diff --git a/Gemfile b/Gemfile
index 1a407cc7020..ea98d098151 100644
--- a/Gemfile
+++ b/Gemfile
@@ -380,7 +380,7 @@ gem 'prometheus-client-mmap', '~> 0.26', '>= 0.26.1', require: 'prometheus/clien
gem 'warning', '~> 1.3.0'
group :development do
- gem 'lefthook', '~> 1.4.6', require: false
+ gem 'lefthook', '~> 1.4.7', require: false
gem 'rubocop'
gem 'solargraph', '~> 0.47.2', require: false
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 3175354b3de..f0f8e77b9c4 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -331,7 +331,7 @@
{"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"},
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
{"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"},
-{"name":"lefthook","version":"1.4.6","platform":"ruby","checksum":"021dd351f03b1fb0024e38454c1ab680c6ea689861612317b6c427c3680e62c5"},
+{"name":"lefthook","version":"1.4.7","platform":"ruby","checksum":"22be305995a871eaad50fe0271457190ff35f929c07705e81076fc91af1f5584"},
{"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"},
{"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"},
{"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 1ba14709bd3..3e03bcfc095 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -936,7 +936,7 @@ GEM
rest-client (~> 2.0)
launchy (2.5.0)
addressable (~> 2.7)
- lefthook (1.4.6)
+ lefthook (1.4.7)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (2.0.0)
@@ -1879,7 +1879,7 @@ DEPENDENCIES
knapsack (~> 1.21.1)
kramdown (~> 2.3.1)
kubeclient (~> 4.11.0)
- lefthook (~> 1.4.6)
+ lefthook (~> 1.4.7)
letter_opener_web (~> 2.0.0)
license_finder (~> 7.0)
licensee (~> 9.15)
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 1c2be99b393..b427820144d 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -57,7 +57,7 @@ export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index
export const DAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/dast/index', {
anchor: 'enable-automatic-dast-run',
});
-export const DAST_BADGE_TEXT = __('Available on-demand');
+export const DAST_BADGE_TEXT = __('Available on demand');
export const DAST_BADGE_TOOLTIP = __(
'On-demand scans run outside of the DevOps cycle and find vulnerabilities in your projects',
);
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
index 9e77a1d44f4..e723505f01f 100644
--- a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
@@ -16,8 +16,8 @@ import { TYPENAME_USER } from '~/graphql_shared/constants';
import searchUsersQuery from '~/issues/list/queries/search_users.query.graphql';
import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
-import getServiceDeskIssuesQuery from '../queries/get_service_desk_issues.query.graphql';
-import getServiceDeskIssuesCounts from '../queries/get_service_desk_issues_counts.query.graphql';
+import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCounts from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import {
errorFetchingCounts,
errorFetchingIssues,
@@ -59,6 +59,8 @@ export default {
'releasesPath',
'autocompleteAwardEmojisPath',
'hasIterationsFeature',
+ 'hasIssueWeightsFeature',
+ 'hasIssuableHealthStatusFeature',
'groupPath',
'emptyStateSvgPath',
'isProject',
@@ -67,6 +69,13 @@ export default {
'isServiceDeskSupported',
'hasAnyIssues',
],
+ props: {
+ eeSearchTokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
data() {
return {
serviceDeskIssues: [],
@@ -201,6 +210,10 @@ export default {
});
}
+ if (this.eeSearchTokens.length) {
+ tokens.push(...this.eeSearchTokens);
+ }
+
tokens.sort((a, b) => a.title.localeCompare(b.title));
return tokens;
diff --git a/app/assets/javascripts/service_desk/index.js b/app/assets/javascripts/service_desk/index.js
index bcb5093e401..440f7748461 100644
--- a/app/assets/javascripts/service_desk/index.js
+++ b/app/assets/javascripts/service_desk/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
+import ServiceDeskListApp from 'ee_else_ce/service_desk/components/service_desk_list_app.vue';
import { gqlClient } from './graphql';
-import ServiceDeskListApp from './components/service_desk_list_app.vue';
export async function mountServiceDeskListApp() {
const el = document.querySelector('.js-service-desk-list');
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
index efd93e88fa9..28e50dceb48 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
@@ -36,7 +36,7 @@ export default {
<style scoped>
.fake-input {
- top: 12px;
- left: 33px;
+ top: 18px;
+ left: 39px;
}
</style>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index bec8c191b31..a91e41585a8 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -290,10 +290,10 @@ export default {
<form
role="search"
:aria-label="searchPlaceholder"
- class="gl-relative gl-rounded-base gl-w-full"
+ class="gl-relative gl-rounded-base gl-w-full gl-pb-0"
data-testid="global-search-form"
>
- <div class="gl-p-1 gl-relative">
+ <div class="gl-relative gl-bg-white gl-border-b gl-mb-n1 gl-p-3">
<gl-search-box-by-type
id="search"
ref="searchInput"
@@ -347,7 +347,7 @@ export default {
<div
ref="resultsList"
data-testid="global-search-results"
- class="global-search-results gl-overflow-y-auto gl-w-full gl-pb-2"
+ class="global-search-results gl-overflow-y-auto gl-w-full gl-pb-3"
@keydown="onKeydown"
>
<command-palette-items
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue
index cd623200b03..2686d86732e 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue
@@ -23,9 +23,6 @@ export default {
computed: {
...mapState(['search', 'loading', 'autocompleteError']),
...mapGetters(['autocompleteGroupedSearchOptions', 'scopedSearchOptions']),
- isPrecededByScopedOptions() {
- return this.scopedSearchOptions.length > 1;
- },
},
methods: {
highlightedName(val) {
@@ -40,9 +37,9 @@ export default {
<div>
<ul v-if="!loading" class="gl-m-0 gl-p-0 gl-list-style-none">
<gl-disclosure-dropdown-group
- v-for="group in autocompleteGroupedSearchOptions"
+ v-for="(group, index) in autocompleteGroupedSearchOptions"
:key="group.name"
- :class="{ 'gl-mt-0!': !isPrecededByScopedOptions }"
+ :class="{ 'gl-mt-0!': index === 0 }"
:group="group"
bordered
>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 37df45a72a4..a4d50466f8f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import Tracking from '~/tracking';
+import axios from '~/lib/utils/axios_utils';
import {
EVENT_ACTION,
EVENT_LABEL_VIEWER,
@@ -53,6 +54,11 @@ export default {
};
},
computed: {
+ isLfsBlob() {
+ const { storedExternally, externalStorage, simpleViewer } = this.blob;
+
+ return storedExternally && externalStorage === 'lfs' && simpleViewer?.fileType === 'text';
+ },
splitContent() {
return this.content.split(/\r?\n/);
},
@@ -83,6 +89,15 @@ export default {
},
},
async created() {
+ if (this.isLfsBlob) {
+ await axios
+ .get(this.blob.externalStorageUrl || this.blob.rawPath)
+ .then((result) => {
+ this.content = result.data;
+ })
+ .catch(() => this.$emit('error'));
+ }
+
addBlobLinksTracking();
this.trackEvent(EVENT_LABEL_VIEWER);
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 12801b272e8..2586f544d94 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -294,8 +294,8 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
.search-scope-help {
- top: 0.625rem;
- right: 2.5rem;
+ top: 1rem;
+ right: 3rem;
}
.gl-search-box-by-type-input-borderless {
@@ -304,5 +304,14 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
.global-search-results {
max-height: 30rem;
+
+ .gl-new-dropdown-item {
+ @include gl-px-3;
+ }
+
+ // Target groups
+ [id*='gl-disclosure-dropdown-group'] {
+ @include gl-px-5;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 005fbc8b058..2722893d04c 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -222,6 +222,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.discussion-reply-holder {
border: 1px solid $border-color;
+ background-color: $white;
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 06516a555ef..6aca2b5646c 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -87,7 +87,7 @@
}
.settings-section,
-.settings-section-no-bottom + .settings-section {
+.settings-section-no-bottom ~ .settings-section {
@include gl-pt-0;
}
@@ -95,7 +95,7 @@
@include gl-pt-6;
}
-.settings-section:not(.settings-section-no-bottom) + .settings-section {
+.settings-section:not(.settings-section-no-bottom) ~ .settings-section {
@include gl-border-t;
}
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
index 53dd06ce638..bd6f4ea84ca 100644
--- a/app/controllers/concerns/integrations/params.rb
+++ b/app/controllers/concerns/integrations/params.rb
@@ -102,10 +102,14 @@ module Integrations
param_values = return_value[:integration]
if param_values.is_a?(ActionController::Parameters)
- if %w[update test].include?(action_name) && integration.chat? &&
- param_values['webhook'] == BaseChatNotification::SECRET_MASK
+ if %w[update test].include?(action_name) && integration.chat?
+ param_values.delete('webhook') if param_values['webhook'] == BaseChatNotification::SECRET_MASK
- param_values.delete('webhook')
+ if integration.try(:mask_configurable_channels?)
+ integration.event_channel_names.each do |channel|
+ param_values.delete(channel) if param_values[channel] == BaseChatNotification::SECRET_MASK
+ end
+ end
end
integration.secret_fields.each do |param|
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index c955032308c..e6e8cfc4020 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -60,7 +60,7 @@ class GraphqlController < ApplicationController
urgency :low, [:execute]
def execute
- result = if Feature.enabled?(:cache_introspection_query) && introspection_query?
+ result = if introspection_query?
execute_introspection_query
else
multiplex? ? execute_multiplex : execute_query
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 7213bd074fc..af0f1bd6808 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -2,6 +2,7 @@
module EmailsHelper
include AppearancesHelper
+ include SafeFormatHelper
# Google Actions
# https://developers.google.com/gmail/markup/reference/go-to-action
@@ -236,6 +237,44 @@ module EmailsHelper
end
end
+ def member_about_to_expire_text(member_source, days_to_expire, format: nil)
+ days_formatted = pluralize(days_to_expire, 'day')
+
+ case member_source
+ when Project
+ url = project_url(member_source)
+ when Group
+ url = group_url(member_source)
+ end
+
+ case format
+ when :html
+ link_to = generate_link(member_source.human_name, url).html_safe
+ safe_format(_("Your membership in %{link_to} %{project_or_group_name} will expire in %{days_formatted}."), link_to: link_to, project_or_group_name: member_source.model_name.singular, days_formatted: days_formatted)
+ else
+ _("Your membership in %{project_or_group} %{project_or_group_name} will expire in %{days_formatted}.") % { project_or_group: member_source.human_name, project_or_group_name: member_source.model_name.singular, days_formatted: days_formatted }
+ end
+ end
+
+ def member_about_to_expire_link(member, member_source, format: nil)
+ project_or_group = member_source.human_name
+
+ case member_source
+ when Project
+ url = project_project_members_url(member_source, search: member.user.username)
+ when Group
+ url = group_group_members_url(member_source, search: member.user.username)
+ end
+
+ case format
+ when :html
+ link_to = generate_link("#{member_source.class.name.downcase} membership", url).html_safe
+ safe_format(_('For additional information, review your %{link_to} or contact your %{project_or_group} owner.'), link_to: link_to, project_or_group: project_or_group)
+ else
+ _('For additional information, review your %{project_or_group} membership: %{url} or contact your %{project_or_group} owner.') % { project_or_group: project_or_group, url: url }
+ end
+ end
+
def group_membership_expiration_changed_text(member, group)
if member.expires?
days = (member.expires_at - Date.today).to_i
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 33c955f94ee..221d359c8c6 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -133,6 +133,22 @@ module Emails
subject: subject(subject))
end
+ def member_about_to_expire_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+
+ return unless member_exists?
+ return unless member.expires_at
+
+ @days_to_expire = (member.expires_at - Date.today).to_i
+
+ return if @days_to_expire <= 0
+
+ email_with_layout(
+ to: member.user.notification_email_for(notification_group),
+ subject: subject(s_("Your membership will expire in %{days_to_expire} days") % { days_to_expire: @days_to_expire }))
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def member
@member ||= Member.find_by(id: @member_id)
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 93d4625c344..4c6ae930cc5 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -166,6 +166,13 @@ class NotifyPreview < ActionMailer::Preview
Notify.member_invited_email('project', member.id, '1234').message
end
+ def member_about_to_expire_email
+ cleanup do
+ member = project.add_member(user, Gitlab::Access::GUEST, expires_at: 7.days.from_now.to_date)
+ Notify.member_about_to_expire_email('project', member.id).message
+ end
+ end
+
def pages_domain_enabled_email
cleanup do
pages_domain = PagesDomain.new(domain: 'my.example.com', project: project, verified_at: Time.now, enabled_until: 1.week.from_now)
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index c9de4d2b3bb..7140e57961f 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -23,7 +23,6 @@ module Integrations
].freeze
SECRET_MASK = '************'
- CHANNEL_LIMIT_PER_EVENT = 10
attribute :category, default: 'chat'
@@ -186,6 +185,14 @@ module Integrations
true
end
+ def channel_limit_per_event
+ 10
+ end
+
+ def mask_configurable_channels?
+ false
+ end
+
private
def should_execute?(object_kind)
@@ -314,13 +321,13 @@ module Integrations
def validate_channel_limit
supported_events.each do |event|
count = channels_for_event(event).count
- next unless count > CHANNEL_LIMIT_PER_EVENT
+ next unless count > channel_limit_per_event
errors.add(
event_channel_name(event).to_sym,
format(
s_('SlackIntegration|cannot have more than %{limit} channels'),
- limit: CHANNEL_LIMIT_PER_EVENT
+ limit: channel_limit_per_event
)
)
end
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 061c491034d..99072179c8c 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -10,7 +10,7 @@ module Integrations
field :webhook,
section: SECTION_TYPE_CONNECTION,
- help: 'e.g. https://discordapp.com/api/webhooks/…',
+ help: 'e.g. https://discord.com/api/webhooks/…',
required: true
field :notify_only_broken_pipelines,
@@ -45,7 +45,7 @@ module Integrations
end
def default_channel_placeholder
- # No-op.
+ s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)')
end
def self.supported_events
@@ -72,10 +72,23 @@ module Integrations
]
end
+ def configurable_channels?
+ true
+ end
+
+ def channel_limit_per_event
+ 1
+ end
+
+ def mask_configurable_channels?
+ true
+ end
+
private
def notify(message, opts)
- client = Discordrb::Webhooks::Client.new(url: webhook)
+ webhook_url = opts[:channel]&.first || webhook
+ client = Discordrb::Webhooks::Client.new(url: webhook_url)
client.execute do |builder|
builder.add_embed do |embed|
diff --git a/app/models/member.rb b/app/models/member.rb
index f164ea244b4..cdf40eaa8f5 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -153,6 +153,7 @@ class Member < ApplicationRecord
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 :expiring_and_not_notified, ->(date) { where("expiry_notified_at is null AND expires_at >= ? AND expires_at <= ?", Date.current, date) }
scope :created_today, -> do
now = Date.current
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index 4bf37e228ab..6d0e7c35865 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -5,7 +5,7 @@ module Ml
validates :project, :model, presence: true
validates :version,
- format: Gitlab::Regex.ml_model_version_regex,
+ format: Gitlab::Regex.semver_regex,
uniqueness: { scope: [:project, :model_id] },
presence: true,
length: { maximum: 255 }
diff --git a/app/serializers/integrations/event_entity.rb b/app/serializers/integrations/event_entity.rb
index 1cbd6114581..f7cac23f30c 100644
--- a/app/serializers/integrations/event_entity.rb
+++ b/app/serializers/integrations/event_entity.rb
@@ -23,7 +23,10 @@ module Integrations
integration.event_channel_name(event)
end
expose :value do |event|
- integration.event_channel_value(event)
+ value = integration.event_channel_value(event)
+ next BaseChatNotification::SECRET_MASK if value.present? && integration.mask_configurable_channels?
+
+ value
end
expose :placeholder do |_event|
integration.default_channel_placeholder
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index b2c0fffc12d..3a3d0e53aae 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -36,6 +36,7 @@ module Members
member.attributes = params
return unless member.changed?
+ member.expiry_notified_at = nil if member.expires_at_changed?
member.tap(&:save!)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index d305c8c03cf..ceafebddfcf 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -529,6 +529,12 @@ class NotificationService
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
+ def member_about_to_expire(member)
+ return true unless member.notifiable?(:mention)
+
+ mailer.member_about_to_expire_email(member.real_source_type, member.id).deliver_later
+ end
+
# Group invite
def invite_group_member(group_member, token)
mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 472ba2f84a0..4979f7e28e7 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -1,49 +1,49 @@
-.gl-border-b.gl-pb-3.gl-mb-6
- .row
- .col-lg-4
- %h4.gl-mt-0
+.settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0
= s_('AdminUsers|Access')
- .col-lg-8
- .form-group.gl-form-group{ role: 'group' }
- = f.label :projects_limit, class: 'gl-display-block col-form-label'
- = f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input'
-
- .form-group.gl-form-group{ role: 'group' }
- = f.gitlab_ui_checkbox_component :can_create_group, s_('AdminUsers|Can create group')
- = f.gitlab_ui_checkbox_component :private_profile, s_('AdminUsers|Private profile')
-
- %fieldset.form-group.gl-form-group
- %legend.col-form-label.col-form-label
- = s_('AdminUsers|Access level')
- - editing_current_user = (current_user == @user)
-
- = f.gitlab_ui_radio_component :access_level, :regular,
- s_('AdminUsers|Regular'),
- radio_options: { disabled: editing_current_user },
- help_text: s_('AdminUsers|Regular users have access to their groups and projects.')
-
- = render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user
-
- - help_text = s_('AdminUsers|The user has unlimited access to all groups, projects, users, and features.')
- - help_text += ' ' + s_('AdminUsers|You cannot remove your own administrator access.') if editing_current_user
- = f.gitlab_ui_radio_component :access_level, :admin,
- s_('AdminUsers|Administrator'),
- radio_options: { disabled: editing_current_user },
- help_text: help_text
-
- .form-group.gl-form-group{ role: 'group' }
- = f.gitlab_ui_checkbox_component :external,
- s_('AdminUsers|External'),
- help_text: s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
- .hidden{ data: user_internal_regex_data }
- .gl-display-flex.gl-align-items-baseline
- %row.hidden#warning_external_automatically_set
- = gl_badge_tag s_('AdminUsers|Automatically marked as default internal user'), variant: :warning
-
- .form-group.gl-form-group{ role: 'group' }
- - @user.credit_card_validation || @user.build_credit_card_validation
- = f.fields_for :credit_card_validation do |ff|
- = ff.gitlab_ui_checkbox_component :credit_card_validated_at,
- s_('AdminUsers|Validate user account'),
- help_text: s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user. Validated users can use free CI minutes on shared runners.'),
- checkbox_options: { checked: @user.credit_card_validated_at.present? }
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :projects_limit, class: 'gl-display-block col-form-label'
+ = f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input gl-form-input-sm'
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.gitlab_ui_checkbox_component :can_create_group, s_('AdminUsers|Can create group')
+ = f.gitlab_ui_checkbox_component :private_profile, s_('AdminUsers|Private profile')
+
+ %fieldset.form-group.gl-form-group
+ %legend.col-form-label.col-form-label
+ = s_('AdminUsers|Access level')
+ - editing_current_user = (current_user == @user)
+
+ = f.gitlab_ui_radio_component :access_level, :regular,
+ s_('AdminUsers|Regular'),
+ radio_options: { disabled: editing_current_user },
+ help_text: s_('AdminUsers|Regular users have access to their groups and projects.')
+
+ = render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user
+
+ - help_text = s_('AdminUsers|The user has unlimited access to all groups, projects, users, and features.')
+ - help_text += ' ' + s_('AdminUsers|You cannot remove your own administrator access.') if editing_current_user
+ = f.gitlab_ui_radio_component :access_level, :admin,
+ s_('AdminUsers|Administrator'),
+ radio_options: { disabled: editing_current_user },
+ help_text: help_text
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.gitlab_ui_checkbox_component :external,
+ s_('AdminUsers|External'),
+ help_text: s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
+ .hidden{ data: user_internal_regex_data }
+ .gl-display-flex.gl-align-items-baseline
+ %row.hidden#warning_external_automatically_set
+ = gl_badge_tag s_('AdminUsers|Automatically marked as default internal user'), variant: :warning
+
+ .form-group.gl-form-group{ role: 'group' }
+ - @user.credit_card_validation || @user.build_credit_card_validation
+ = f.fields_for :credit_card_validation do |ff|
+ = ff.gitlab_ui_checkbox_component :credit_card_validated_at,
+ s_('AdminUsers|Validate user account'),
+ help_text: s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user. Validated users can use free CI minutes on shared runners.'),
+ checkbox_options: { checked: @user.credit_card_validated_at.present? }
diff --git a/app/views/admin/users/_admin_notes.html.haml b/app/views/admin/users/_admin_notes.html.haml
index dce008afb26..85796246c83 100644
--- a/app/views/admin/users/_admin_notes.html.haml
+++ b/app/views/admin/users/_admin_notes.html.haml
@@ -1,9 +1,9 @@
-.gl-mb-3
- .row
- .col-lg-4
- %h4.gl-mt-0
+.settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0
= _('Admin notes')
- .col-lg-8
- .form-group.gl-form-group{ role: 'group' }
- = f.label :note, s_('Admin|Note')
- = f.text_area :note, class: 'form-control gl-form-input gl-form-textarea'
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :note, s_('Admin|Note')
+ = f.text_area :note, class: 'form-control gl-form-input gl-form-textarea'
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 8822d52c3c0..ffe7e128d60 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -2,42 +2,42 @@
= gitlab_ui_form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
= form_errors(@user)
- .gl-border-b.gl-pb-3.gl-mb-6
- .row
- .col-lg-4
- %h4.gl-mt-0
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0
= _('Account')
- .col-lg-8
- .form-group.gl-form-group{ role: 'group' }
- = f.label :name, _('Name'), class: 'gl-display-block col-form-label'
- = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
-
- .form-group.gl-form-group{ role: 'group' }
- = f.label :username, _('Username'), class: 'gl-display-block col-form-label'
- = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control gl-form-input'
-
- .form-group.gl-form-group{ role: 'group' }
- = f.label :email, _('Email'), class: 'gl-display-block col-form-label'
- = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
-
- .gl-border-b.gl-pb-3.gl-mb-6
- .row
- .col-lg-4
- %h4.gl-mt-0
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :name, _('Name'), class: 'gl-display-block col-form-label'
+ = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control gl-form-input gl-form-input-lg'
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :username, _('Username'), class: 'gl-display-block col-form-label'
+ = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control gl-form-input gl-form-input-lg'
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :email, _('Email'), class: 'gl-display-block col-form-label'
+ = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control gl-form-input gl-form-input-lg'
+
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0
= _('Password')
- .col-lg-8
- - if @user.new_record?
- = render Pajamas::AlertComponent.new(variant: :info, dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
- - c.with_body do
- = s_('AdminUsers|Reset link will be generated and sent to the user. User will be forced to set the password on first sign in.')
- - else
- .form-group.gl-form-group{ role: 'group' }
- = f.label :password, _('Password'), class: 'gl-display-block col-form-label'
- = f.password_field :password, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input js-password-complexity-validation'
- = render_if_exists 'shared/password_requirements_list'
- .form-group.gl-form-group{ role: 'group' }
- = f.label :password_confirmation, _('Password confirmation'), class: 'gl-display-block col-form-label'
- = f.password_field :password_confirmation, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
+
+ - if @user.new_record?
+ = render Pajamas::AlertComponent.new(variant: :info, dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
+ - c.with_body do
+ = s_('AdminUsers|Reset link will be generated and sent to the user. User will be forced to set the password on first sign in.')
+ - else
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :password, _('Password'), class: 'gl-display-block col-form-label'
+ = f.password_field :password, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input js-password-complexity-validation gl-form-input-lg'
+ = render_if_exists 'shared/password_requirements_list'
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :password_confirmation, _('Password confirmation'), class: 'gl-display-block col-form-label'
+ = f.password_field :password_confirmation, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input gl-form-input-lg'
= render partial: 'access_levels', locals: { f: f }
@@ -45,42 +45,42 @@
= render_if_exists 'admin/users/limits', f: f
- .gl-border-b.gl-pb-6.gl-mb-6
- .row
- .col-lg-4
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
%h4.gl-mt-0
= _('Profile')
- .col-lg-8
- .form-group.gl-form-group{ role: 'group' }
- = f.label :avatar, s_('AdminUsers|Avatar'), class: 'gl-display-block col-form-label'
- = f.file_field :avatar
- .form-group.gl-form-group{ role: 'group' }
- = f.label :skype, s_('AdminUsers|Skype'), class: 'gl-display-block col-form-label'
- = f.text_field :skype, class: 'form-control gl-form-input'
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :avatar, s_('AdminUsers|Avatar'), class: 'gl-display-block col-form-label gl-form-input-lg'
+ = f.file_field :avatar
+
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :skype, s_('AdminUsers|Skype'), class: 'gl-display-block col-form-label'
+ = f.text_field :skype, class: 'form-control gl-form-input gl-form-input-lg'
- .form-group.gl-form-group{ role: 'group' }
- = f.label :linkedin, s_('AdminUsers|Linkedin'), class: 'gl-display-block col-form-label'
- = f.text_field :linkedin, class: 'form-control gl-form-input'
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :linkedin, s_('AdminUsers|Linkedin'), class: 'gl-display-block col-form-label'
+ = f.text_field :linkedin, class: 'form-control gl-form-input gl-form-input-lg'
- .form-group.gl-form-group{ role: 'group' }
- = f.label :twitter, _('Twitter'), class: 'gl-display-block col-form-label'
- = f.text_field :twitter, class: 'form-control gl-form-input'
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :twitter, _('Twitter'), class: 'gl-display-block col-form-label'
+ = f.text_field :twitter, class: 'form-control gl-form-input gl-form-input-lg'
- .form-group.gl-form-group{ role: 'group' }
- = f.label :website_url, s_('AdminUsers|Website URL'), class: 'gl-display-block col-form-label'
- = f.text_field :website_url, class: 'form-control gl-form-input'
+ .form-group.gl-form-group{ role: 'group' }
+ = f.label :website_url, s_('AdminUsers|Website URL'), class: 'gl-display-block col-form-label'
+ = f.text_field :website_url, class: 'form-control gl-form-input gl-form-input-lg'
= render_if_exists 'admin/users/custom_attributes', f: f
= render 'admin/users/admin_notes', f: f
- %div
+ .settings-sticky-footer
- if @user.new_record?
- = f.submit _('Create user'), pajamas_button: true
+ = f.submit _('Create user'), pajamas_button: true, class: 'gl-mr-3'
= render Pajamas::ButtonComponent.new(href: admin_users_path) do
= _('Cancel')
- else
- = f.submit _('Save changes'), pajamas_button: true
+ = f.submit _('Save changes'), pajamas_button: true, class: 'gl-mr-3'
= render Pajamas::ButtonComponent.new(href: admin_user_path(@user)) do
= _('Cancel')
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 4825f192d4d..345a1cc0225 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -21,7 +21,8 @@
= recaptcha_tags nonce: content_security_policy_nonce
- if remember_me_enabled?
- = f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { autocomplete: 'off' }
+ .form-group
+ = f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { autocomplete: 'off' }
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { class: "js-sign-in-button #{'js-no-auto-disable' if Feature.enabled?(:arkose_labs_login_challenge)}", data: { qa_selector: 'sign_in_button', testid: 'sign-in-button' } }) do
= _('Sign in')
diff --git a/app/views/notify/member_about_to_expire_email.html.haml b/app/views/notify/member_about_to_expire_email.html.haml
new file mode 100644
index 00000000000..a9f92d90ae6
--- /dev/null
+++ b/app/views/notify/member_about_to_expire_email.html.haml
@@ -0,0 +1,6 @@
+= email_default_heading(say_hi(@member.user))
+
+%p
+ = member_about_to_expire_text(@member_source, @days_to_expire, format: :html)
+%p
+ = member_about_to_expire_link(@member, @member_source, format: :html)
diff --git a/app/views/notify/member_about_to_expire_email.text.erb b/app/views/notify/member_about_to_expire_email.text.erb
new file mode 100644
index 00000000000..0c6e78bf501
--- /dev/null
+++ b/app/views/notify/member_about_to_expire_email.text.erb
@@ -0,0 +1,5 @@
+<%= say_hi(@member.user) %>
+
+<%= member_about_to_expire_text(@member_source, @days_to_expire) %>
+
+<%= member_about_to_expire_link(@member, @member_source) %>
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 4f2d101239b..e2d199b9e51 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -552,6 +552,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: cronjob:members_expiring
+ :worker_name: Members::ExpiringWorker
+ :feature_category: :system_access
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: cronjob:metrics_global_metrics_update
:worker_name: Metrics::GlobalMetricsUpdateWorker
:feature_category: :metrics
@@ -2955,6 +2964,15 @@
:weight: 2
:idempotent:
:tags: []
+- :name: members_expiring_email_notification
+ :worker_name: Members::ExpiringEmailNotificationWorker
+ :feature_category: :system_access
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: merge
:worker_name: MergeWorker
:feature_category: :source_code_management
diff --git a/app/workers/members/expiring_email_notification_worker.rb b/app/workers/members/expiring_email_notification_worker.rb
new file mode 100644
index 00000000000..1d0a6eb254a
--- /dev/null
+++ b/app/workers/members/expiring_email_notification_worker.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Members
+ class ExpiringEmailNotificationWorker # rubocop:disable Scalability/CronWorkerContext
+ include ApplicationWorker
+
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
+ feature_category :system_access
+ urgency :low
+ idempotent!
+
+ def perform(member_id)
+ notification_service = NotificationService.new
+ member = ::Member.find_by_id(member_id)
+
+ return unless member
+ return unless Feature.enabled?(:member_expiring_email_notification, member.source.root_ancestor)
+ return if member.expiry_notified_at.present?
+
+ with_context(user: member.user) do
+ notification_service.member_about_to_expire(member)
+ Gitlab::AppLogger.info(message: "Notifying user about expiring membership", member_id: member.id)
+
+ member.update(expiry_notified_at: Time.current)
+ end
+ end
+ end
+end
diff --git a/app/workers/members/expiring_worker.rb b/app/workers/members/expiring_worker.rb
new file mode 100644
index 00000000000..0d631af3a7c
--- /dev/null
+++ b/app/workers/members/expiring_worker.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Members
+ class ExpiringWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker does not perform work scoped to a context
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ data_consistency :sticky
+ feature_category :system_access
+ urgency :low
+
+ BATCH_LIMIT = 500
+
+ def perform
+ return unless Feature.enabled?(:member_expiring_email_notification)
+
+ limit_date = Member::DAYS_TO_EXPIRE.days.from_now.to_date
+
+ expiring_members = Member.active.where(users: { user_type: :human }).expiring_and_not_notified(limit_date) # rubocop: disable CodeReuse/ActiveRecord
+
+ expiring_members.each_batch(of: BATCH_LIMIT) do |members|
+ members.pluck_primary_key.each do |member_id|
+ Members::ExpiringEmailNotificationWorker.perform_async(member_id)
+ end
+ end
+ end
+ end
+end
diff --git a/config/feature_flags/development/cache_introspection_query.yml b/config/feature_flags/development/member_expiring_email_notification.yml
index d0b12993631..1775cc67b52 100644
--- a/config/feature_flags/development/cache_introspection_query.yml
+++ b/config/feature_flags/development/member_expiring_email_notification.yml
@@ -1,8 +1,8 @@
---
-name: cache_introspection_query
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120279
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412457
-milestone: '16.1'
+name: member_expiring_email_notification
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124577
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416581
+milestone: '16.3'
type: development
-group: group::application performance
+group: group::authentication and authorization
default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 50d26236a29..cda625fab13 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -511,6 +511,9 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
Settings.cron_jobs['ci_archive_traces_cron_worker'] ||= {}
Settings.cron_jobs['ci_archive_traces_cron_worker']['cron'] ||= '17 * * * *'
Settings.cron_jobs['ci_archive_traces_cron_worker']['job_class'] = 'Ci::ArchiveTracesCronWorker'
+Settings.cron_jobs['members_expiring_worker'] ||= {}
+Settings.cron_jobs['members_expiring_worker']['cron'] ||= '0 1 * * *'
+Settings.cron_jobs['members_expiring_worker']['job_class'] = 'Members::ExpiringWorker'
Settings.cron_jobs['remove_expired_members_worker'] ||= {}
Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0205d24d48b..7fe4db435a6 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -345,6 +345,8 @@
- 2
- - mailers
- 2
+- - members_expiring_email_notification
+ - 1
- - merge
- 5
- - merge_request_cleanup_refs
diff --git a/db/migrate/20230707003301_add_expiry_notified_at_to_member.rb b/db/migrate/20230707003301_add_expiry_notified_at_to_member.rb
new file mode 100644
index 00000000000..e890325e5fa
--- /dev/null
+++ b/db/migrate/20230707003301_add_expiry_notified_at_to_member.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddExpiryNotifiedAtToMember < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'members'
+ COLUMN_NAME = 'expiry_notified_at'
+
+ def up
+ with_lock_retries do
+ add_column(TABLE_NAME, COLUMN_NAME, :datetime_with_timezone)
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column TABLE_NAME, COLUMN_NAME
+ end
+ end
+end
diff --git a/db/post_migrate/20230714015909_add_index_for_member_expiring_query.rb b/db/post_migrate/20230714015909_add_index_for_member_expiring_query.rb
new file mode 100644
index 00000000000..4d98d4792af
--- /dev/null
+++ b/db/post_migrate/20230714015909_add_index_for_member_expiring_query.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexForMemberExpiringQuery < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_members_on_expiring_at_access_level_id'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :members,
+ [:expires_at, :access_level, :id],
+ where: 'requested_at IS NULL AND expiry_notified_at IS NULL',
+ name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :members, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230707003301 b/db/schema_migrations/20230707003301
new file mode 100644
index 00000000000..b1d5d84ee1b
--- /dev/null
+++ b/db/schema_migrations/20230707003301
@@ -0,0 +1 @@
+dac0b6b1f86685bd19aed528c75197adfee06154ea68efdb854f4b17deb973fa \ No newline at end of file
diff --git a/db/schema_migrations/20230714015909 b/db/schema_migrations/20230714015909
new file mode 100644
index 00000000000..c5362b4a82d
--- /dev/null
+++ b/db/schema_migrations/20230714015909
@@ -0,0 +1 @@
+54f82e196c756ca60c4d7300ef1987a6b1ca50d4ef87bf89bb62eb577164365c \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d1f21674c21..18e82de6a74 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18018,6 +18018,7 @@ CREATE TABLE members (
invite_email_success boolean DEFAULT true NOT NULL,
member_namespace_id bigint,
member_role_id bigint,
+ expiry_notified_at timestamp with time zone,
CONSTRAINT check_508774aac0 CHECK ((member_namespace_id IS NOT NULL))
);
@@ -31872,6 +31873,8 @@ CREATE INDEX index_members_on_access_level ON members USING btree (access_level)
CREATE INDEX index_members_on_expires_at ON members USING btree (expires_at);
+CREATE INDEX index_members_on_expiring_at_access_level_id ON members USING btree (expires_at, access_level, id) WHERE ((requested_at IS NULL) AND (expiry_notified_at IS NULL));
+
CREATE INDEX index_members_on_invite_email ON members USING btree (invite_email);
CREATE UNIQUE INDEX index_members_on_invite_token ON members USING btree (invite_token);
diff --git a/doc/api/integrations.md b/doc/api/integrations.md
index b1cb6ed6560..aafda55b9f2 100644
--- a/doc/api/integrations.md
+++ b/doc/api/integrations.md
@@ -637,6 +637,8 @@ Send notifications about project events to a Discord channel.
### Create/Edit Discord integration
+> `_channel` parameters [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125621) in GitLab 16.3.
+
Set Discord integration for a project.
```plaintext
@@ -650,15 +652,24 @@ Parameters:
| `webhook` | string | true | Discord webhook. For example, `https://discord.com/api/webhooks/…` |
| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default value is "default" |
| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `confidential_issue_channel` | string | false | The webhook override to receive confidential issues events notifications |
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `confidential_note_channel` | string | false | The webhook override to receive confidential note events notifications |
| `issues_events` | boolean | false | Enable notifications for issue events |
+| `issue_channel` | string | false | The webhook override to receive issues events notifications |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `merge_request_channel` | string | false | The webhook override to receive merge request events notifications |
| `note_events` | boolean | false | Enable notifications for note events |
+| `note_channel` | string | false | The webhook override to receive note events notifications |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `pipeline_channel` | string | false | The webhook override to receive pipeline events notifications |
| `push_events` | boolean | false | Enable notifications for push events |
+| `push_channel` | string | false | The webhook override to receive push events notifications |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `tag_push_channel` | string | false | The webhook override to receive tag push events notifications |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+| `wiki_page_channel` | string | false | The webhook override to receive wiki page events notifications |
### Disable Discord integration
diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
index f45c60bdd1f..ad7de0c93b8 100644
--- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
+++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
@@ -5,13 +5,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: tutorial
---
-# Authenticating and reading secrets with HashiCorp Vault (Deprecated) **(PREMIUM)**
+# Authenticating and reading secrets with HashiCorp Vault **(PREMIUM)**
WARNING:
-Authenticating with HashiCorp Vault by using `CI_JOB_JWT` was [deprecated in GitLab 15.9](../../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
-and the token is scheduled to be removed in GitLab 16.5. This change is a breaking change.
-Use [ID tokens to authenticate with HashiCorp Vault](../../secrets/id_token_authentication.md#automatic-id-token-authentication-with-hashicorp-vault)
-instead.
+Authenticating with `CI_JOB_JWT` was [deprecated in GitLab 15.9](../../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
+and the token is scheduled to be removed in GitLab 16.5. Use
+[ID tokens to authenticate with HashiCorp Vault](../../secrets/id_token_authentication.md#automatic-id-token-authentication-with-hashicorp-vault)
+instead, as demonstrated on this page.
This tutorial demonstrates how to authenticate, configure, and read secrets with HashiCorp's Vault from GitLab CI/CD.
@@ -249,7 +249,7 @@ Now, configure the JWT Authentication method:
```shell
$ vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/-/jwks" \
- bound_issuer="gitlab.example.com"
+ bound_issuer="https://gitlab.example.com"
```
[`bound_issuer`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_issuer) specifies that only a JWT with the issuer (that is, the `iss` claim) set to `gitlab.example.com` can use this method to authenticate, and that the JWKS endpoint (`https://gitlab.example.com/-/jwks`) should be used to validate the token.
diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md
index 8d6f36bb189..2c85cb19fab 100644
--- a/doc/development/secure_coding_guidelines.md
+++ b/doc/development/secure_coding_guidelines.md
@@ -1412,3 +1412,64 @@ If circumstances dictate that local storage is the only option, a couple of prec
- Local storage should only be used for the minimal amount of data possible. Consider alternative storage formats.
- If you have to store sensitive data using local storage, do so for the minimum time possible, calling `localStorage.removeItem` on the item as soon as we're done with it. Another alternative is to call `localStorage.clear()`.
+
+## Logging
+
+Logging is the tracking of events that happen in the system for the purposes of future investigation or processing.
+
+### Purpose of logging
+
+Logging helps track events for debugging. Logging also allows the application to generate an audit trail that you can use for security incident identification and analysis.
+
+### What type of events should be logged
+
+- Failures
+ - Login failures
+ - Input/output validation failures
+ - Authentication failures
+ - Authorization failures
+ - Session management failures
+ - Timeout errors
+- Account lockouts
+- Use of invalid access tokens
+- Authentication and authorization events
+ - Access token creation/revocation/expiry
+ - Configuration changes by administrators
+ - User creation or modification
+ - Password change
+ - User creation
+ - Email change
+- Sensitive operations
+ - Any operation on sensitive files or resources
+ - New runner registration
+
+### What should be captured in the logs
+
+- The application logs must record attributes of the event, which helps auditors identify the time/date, IP, user ID, and event details.
+- To avoid resource depletion, make sure the proper level for logging is used (for example, `information`, `error`, or `fatal`).
+
+### What should not be captured in the logs
+
+- Personal user information.
+- Credentials like access tokens or passwords. If credentials must be captured for debugging purposes, log the internal ID of the credential (if available) instead. Never log credentials under any circumstances.
+ - When [debug logging](../ci/variables/index.md#enable-debug-logging) is enabled, all masked CI/CD variables are visible in job logs. Consider using [protected variables](../ci/variables/index.md#protect-a-cicd-variable) when possible so that sensitive CI/CD variables are only available to pipelines running on protected branches or protected tags.
+- Any data supplied by the user without proper validation.
+- Any information that might be considered sensitive (for example, credentials, passwords, tokens, keys, or secrets). Here is an [example](https://gitlab.com/gitlab-org/gitlab/-/issues/383142) of sensitive information being leaked through logs.
+
+### Protecting log files
+
+- Access to the log files should be restricted so that only the intended party can modify the logs.
+- External user input should not be directly captured in the logs without any validation. This could lead to unintended modification of logs through log injection attacks.
+- An audit trail for log edits must be available.
+- To avoid data loss, logs must be saved on different storage.
+
+### Who to contact if you have questions
+
+For general guidance, contact the [Application Security](https://about.gitlab.com/handbook/security/security-engineering/application-security/) team.
+
+### Related topics
+
+- [Log system in GitLab](../administration/logs/index.md)
+- [Audit event development guidelines](../development/audit_event_guide/index.md))
+- [Security logging overview](https://about.gitlab.com/handbook/security/security-engineering/security-logging/)
+- [OWASP logging cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)
diff --git a/doc/integration/mattermost/index.md b/doc/integration/mattermost/index.md
index d8558647e4b..3459e7974cf 100644
--- a/doc/integration/mattermost/index.md
+++ b/doc/integration/mattermost/index.md
@@ -272,7 +272,7 @@ There are 4 users on local instance
### Use `mmctl` through a remote connection
For remote connections or local connections where the socket cannot be used,
-create a non SSO user and give that user administrator privileges. Those credentials
+create a non-SSO user and give that user administrator privileges. Those credentials
can then be used to authenticate `mmctl`:
```shell
@@ -311,46 +311,41 @@ This setting can also be configured in `/var/opt/gitlab/mattermost/config.json`.
## Upgrading GitLab Mattermost
-Below is a list of Mattermost versions for GitLab 11.10 and later:
-
-| GitLab Version | Mattermost Version |
-| :------------ |:----------------|
-| 11.11 | 5.10 |
-| 12.0 | 5.11 |
-| 12.1 | 5.12 |
-| 12.2 | 5.13 |
-| 12.3 | 5.14 |
-| 12.4 | 5.15 |
-| 12.5 | 5.16 |
-| 12.6 | 5.17 |
-| 12.7 | 5.17 |
-| 12.8 | 5.19 |
-| 12.9 | 5.20 |
-| 12.10 | 5.21 |
-| 13.0 | 5.22 |
-| 13.1 | 5.23 |
-| 13.2 | 5.24 |
-| 13.3 | 5.25 |
-| 13.4 | 5.26 |
-| 13.5 | 5.27 |
-| 13.6 | 5.28 |
-| 13.7 | 5.29 |
-| 13.8 | 5.30 |
-| 13.9 | 5.31 |
-| 13.10 | 5.32 |
-| 13.11 | 5.33 |
-| 13.12 | 5.34 |
-| 14.0 | 5.35 |
-| 14.1 | 5.36 |
-| 14.2 | 5.37 |
-| 14.3 | 5.38 |
-| 14.4 | 5.39 |
-| 14.5 | 5.39 |
-| 14.6 | 6.1 |
-| 14.7 | 6.2 |
-
-- GitLab 14.5 remained on Mattermost 5.39
-- GitLab 14.6 updates to Mattermost 6.1 instead of 6.0
+Below is a list of Mattermost versions for GitLab 14.0 and later:
+
+| GitLab version | Mattermost version |
+|:---------------|:-------------------|
+| 14.0 | 5.35 |
+| 14.1 | 5.36 |
+| 14.2 | 5.37 |
+| 14.3 | 5.38 |
+| 14.4 | 5.39 |
+| 14.5 | 5.39 |
+| 14.6 | 6.1 |
+| 14.7 | 6.2 |
+| 14.8 | 6.3 |
+| 14.9 | 6.4 |
+| 14.10 | 6.5 |
+| 15.0 | 6.6 |
+| 15.1 | 6.7 |
+| 15.2 | 7.0 |
+| 15.3 | 7.1 |
+| 15.4 | 7.2 |
+| 15.5 | 7.3 |
+| 15.6 | 7.4 |
+| 15.7 | 7.5 |
+| 15.8 | 7.5 |
+| 15.9 | 7.7 |
+| 15.10 | 7.8 |
+| 15.11 | 7.9 |
+| 16.0 | 7.10 |
+| 16.1 | 7.10 |
+| 16.2 | 7.10 |
+
+- GitLab 14.5 remained on Mattermost 5.39.
+- GitLab 14.6 updates to Mattermost 6.1 instead of 6.0.
+- GitLab 15.8 remained on Mattermost 7.5.
+- GitLab 16.1 and 16.2 remained on Mattermost 7.10.
NOTE:
When upgrading the Mattermost version, it is essential to check the
diff --git a/doc/integration/vault.md b/doc/integration/vault.md
index c93e3e53949..3e408f2c9ae 100644
--- a/doc/integration/vault.md
+++ b/doc/integration/vault.md
@@ -84,7 +84,7 @@ and scopes given to GitLab when you created the application.
Run the following command in the terminal:
```shell
-vault write auth/oidc/role/demo -<<EOF
+vault write auth/oidc/role/demo - <<EOF
{
"user_claim": "sub",
"allowed_redirect_uris": "<your_vault_instance_redirect_uris>",
diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md
index 180e39e3254..dca10aa732b 100644
--- a/doc/user/project/integrations/discord_notifications.md
+++ b/doc/user/project/integrations/discord_notifications.md
@@ -24,14 +24,18 @@ and configure it in GitLab.
## Configure created webhook in GitLab
+> Event webhook overrides [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125621) in GitLab 16.3.
+
With the webhook URL created in the Discord channel, you can set up the Discord Notifications integration in GitLab.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Settings > Integrations**.
1. Select **Discord Notifications**.
1. Ensure that the **Active** toggle is enabled.
-1. Check the checkboxes corresponding to the GitLab events for which you want to send notifications to Discord.
-1. Paste the webhook URL that you copied from the create Discord webhook step.
+1. Paste the webhook URL that you [created earlier](#create-webhook) into the **Webhook** field.
+1. Select the checkboxes corresponding to the GitLab events for which you want to send notifications to Discord.
+1. Optionally for each checkbox that you select, enter a new Discord webhook URL that you have [configured](#create-webhook)
+ to override the default one in the **Webhook** field.
1. Configure the remaining options and select the **Save changes** button.
The Discord channel you created the webhook for now receives notification of the GitLab events that were configured.
diff --git a/doc/user/project/ml/experiment_tracking/mlflow_client.md b/doc/user/project/ml/experiment_tracking/mlflow_client.md
index 7fd5c7cca92..d83c0346415 100644
--- a/doc/user/project/ml/experiment_tracking/mlflow_client.md
+++ b/doc/user/project/ml/experiment_tracking/mlflow_client.md
@@ -76,30 +76,30 @@ candidate metadata. To associate a candidate to a CI/CD job:
GitLab supports these methods from the MLflow client. Other methods might be supported but were not
tested. More information can be found in the [MLflow Documentation](https://www.mlflow.org/docs/1.28.0/python_api/mlflow.html).
-| Method | Supported | Version Added | Comments |
-|--------------------------|------------------|----------------|----------|
-| `get_experiment` | Yes | 15.11 | |
-| `get_experiment_by_name` | Yes | 15.11 | |
-| `set_experiment` | Yes | 15.11 | |
-| `get_run` | Yes | 15.11 | |
-| `start_run` | Yes | 15.11 | |
-| `log_artifact` | Yes with caveat | 15.11 | (15.11) `artifact_path` must be empty string. Does not support directories.
-| `log_artifacts` | Yes with caveat | 15.11 | (15.11) `artifact_path` must be empty string. Does not support directories.
-| `log_batch` | Yes | 15.11 | |
-| `log_metric` | Yes | 15.11 | |
-| `log_metrics` | Yes | 15.11 | |
-| `log_param` | Yes | 15.11 | |
-| `log_params` | Yes | 15.11 | |
-| `log_figure` | Yes | 15.11 | |
-| `log_image` | Yes | 15.11 | |
-| `log_text` | Yes with caveat | 15.11 | (15.11) Does not support directories.
-| `log_dict` | Yes with caveat | 15.11 | (15.11) Does not support directories.
-| `set_tag` | Yes | 15.11 | |
-| `set_tags` | Yes | 15.11 | |
-| `set_terminated` | Yes | 15.11 | |
-| `end_run` | Yes | 15.11 | |
-| `update_run` | Yes | 15.11 | |
-| `log_model` | Partial | 15.11 | (15.11) Saves the artifacts, but not the model data. `artifact_path` must be empty.
+| Method | Supported | Version Added | Comments |
+|--------------------------|------------------|----------------|-------------------------------------------------------------------------------------|
+| `get_experiment` | Yes | 15.11 | |
+| `get_experiment_by_name` | Yes | 15.11 | |
+| `set_experiment` | Yes | 15.11 | |
+| `get_run` | Yes | 15.11 | |
+| `start_run` | Yes | 15.11 | (16.3) If a name is not provided, the candidate receives a random nickname. |
+| `log_artifact` | Yes with caveat | 15.11 | (15.11) `artifact_path` must be empty. Does not support directories. |
+| `log_artifacts` | Yes with caveat | 15.11 | (15.11) `artifact_path` must be empty. Does not support directories. |
+| `log_batch` | Yes | 15.11 | |
+| `log_metric` | Yes | 15.11 | |
+| `log_metrics` | Yes | 15.11 | |
+| `log_param` | Yes | 15.11 | |
+| `log_params` | Yes | 15.11 | |
+| `log_figure` | Yes | 15.11 | |
+| `log_image` | Yes | 15.11 | |
+| `log_text` | Yes with caveat | 15.11 | (15.11) Does not support directories. |
+| `log_dict` | Yes with caveat | 15.11 | (15.11) Does not support directories. |
+| `set_tag` | Yes | 15.11 | |
+| `set_tags` | Yes | 15.11 | |
+| `set_terminated` | Yes | 15.11 | |
+| `end_run` | Yes | 15.11 | |
+| `update_run` | Yes | 15.11 | |
+| `log_model` | Partial | 15.11 | (15.11) Saves the artifacts, but not the model data. `artifact_path` must be empty. |
## Limitations
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index 09dd69ef03b..d9c3d95e12d 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -453,7 +453,8 @@ module API
desc: 'Branches for which notifications are to be sent'
},
chat_notification_flags,
- chat_notification_events
+ chat_notification_events,
+ chat_notification_channels
].flatten,
'drone-ci' => [
{
diff --git a/lib/api/ml_model_packages.rb b/lib/api/ml_model_packages.rb
index 35d231d9fe1..8a7a8fc9525 100644
--- a/lib/api/ml_model_packages.rb
+++ b/lib/api/ml_model_packages.rb
@@ -50,7 +50,7 @@ module API
requires :model_name, type: String, desc: 'Model name', regexp: Gitlab::Regex.ml_model_name_regex,
file_path: true
requires :model_version, type: String, desc: 'Model version',
- regexp: Gitlab::Regex.ml_model_version_regex
+ regexp: Gitlab::Regex.semver_regex
requires :file_name, type: String, desc: 'Package file name',
regexp: Gitlab::Regex.ml_model_file_name_regex, file_path: true
optional :status, type: String, values: ALLOWED_STATUSES, desc: 'Package status'
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index ed1f134ff70..c5b93cf58d6 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,312 +2,10 @@
module Gitlab
module Regex
- module Packages
- CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
- CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
-
- PYPI_NORMALIZED_NAME_REGEX_STRING = '[-_.]+'
-
- # see https://github.com/apache/maven/blob/c1dfb947b509e195c75d4275a113598cf3063c3e/maven-artifact/src/main/java/org/apache/maven/artifact/Artifact.java#L46
- MAVEN_SNAPSHOT_DYNAMIC_PARTS = /\A.{0,1000}(-\d{8}\.\d{6}-\d+).{0,1000}\z/.freeze
-
- API_PATH_REGEX = %r{^/api/v\d+/(projects/[^/]+/|groups?/[^/]+/-/)?packages/[A-Za-z]+}.freeze
-
- def conan_package_reference_regex
- @conan_package_reference_regex ||= %r{\A[A-Za-z0-9]+\z}.freeze
- end
-
- def conan_revision_regex
- @conan_revision_regex ||= %r{\A0\z}.freeze
- end
-
- def conan_recipe_user_channel_regex
- %r{\A(_|#{conan_name_regex})\z}.freeze
- end
-
- def conan_recipe_component_regex
- # https://docs.conan.io/en/latest/reference/conanfile/attributes.html#name
- @conan_recipe_component_regex ||= %r{\A#{conan_name_regex}\z}.freeze
- end
-
- def composer_package_version_regex
- # see https://github.com/composer/semver/blob/31f3ea725711245195f62e54ffa402d8ef2fdba9/src/VersionParser.php#L215
- @composer_package_version_regex ||= %r{\Av?((\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?)?\z}.freeze
- end
-
- def composer_dev_version_regex
- @composer_dev_version_regex ||= %r{(^dev-)|(-dev$)}.freeze
- end
-
- def package_name_regex
- @package_name_regex ||=
- %r{
- \A\@?
- (?> # atomic group to prevent backtracking
- (([\w\-\.\+]*)\/)*([\w\-\.]+)
- )
- @?
- (?> # atomic group to prevent backtracking
- (([\w\-\.\+]*)\/)*([\w\-\.]*)
- )
- \z
- }x.freeze
- end
-
- def maven_file_name_regex
- @maven_file_name_regex ||= %r{\A[A-Za-z0-9\.\_\-\+]+\z}.freeze
- end
-
- def maven_path_regex
- @maven_path_regex ||= %r{\A\@?(([\w\-\.]*)/)*([\w\-\.\+]*)\z}.freeze
- end
-
- def maven_app_name_regex
- @maven_app_name_regex ||= /\A[\w\-\.]+\z/.freeze
- end
-
- def maven_version_regex
- @maven_version_regex ||= /\A(?!.*\.\.)[\w+.-]+\z/.freeze
- end
-
- def maven_app_group_regex
- maven_app_name_regex
- end
-
- def npm_package_name_regex
- @npm_package_name_regex ||= %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9]+\z}o
- end
-
- def npm_package_name_regex_message
- 'should be a valid NPM package name: https://github.com/npm/validate-npm-package-name#naming-rules.'
- end
-
- def nuget_package_name_regex
- @nuget_package_name_regex ||= %r{\A[-+\.\_a-zA-Z0-9]+\z}.freeze
- end
-
- def nuget_version_regex
- @nuget_version_regex ||= /
- \A#{_semver_major_regex}
- \.#{_semver_minor_regex}
- (\.#{_semver_patch_regex})?
- (\.\d*)?
- #{_semver_prerelease_build_regex}\z
- /x.freeze
- end
-
- def terraform_module_package_name_regex
- @terraform_module_package_name_regex ||= %r{\A[-a-z0-9]+\/[-a-z0-9]+\z}.freeze
- end
-
- def pypi_version_regex
- # See the official regex: https://github.com/pypa/packaging/blob/16.7/packaging/version.py#L159
-
- @pypi_version_regex ||= %r{
- \A(?:
- v?
- (?:([0-9]+)!)? (?# epoch)
- ([0-9]+(?:\.[0-9]+)*) (?# release segment)
- ([-_\.]?((a|b|c|rc|alpha|beta|pre|preview))[-_\.]?([0-9]+)?)? (?# pre-release)
- ((?:-([0-9]+))|(?:[-_\.]?(post|rev|r)[-_\.]?([0-9]+)?))? (?# post release)
- ([-_\.]?(dev)[-_\.]?([0-9]+)?)? (?# dev release)
- (?:\+([a-z0-9]+(?:[-_\.][a-z0-9]+)*))? (?# local version)
- )\z}xi.freeze
- end
-
- def debian_package_name_regex
- # See official parser
- # https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n122
- # @debian_package_name_regex ||= %r{\A[a-z0-9][-+\._a-z0-9]*\z}i.freeze
- # But we prefer a more strict version from Lintian
- # https://salsa.debian.org/lintian/lintian/-/blob/5080c0068ffc4a9ddee92022a91d0c2ff53e56d1/lib/Lintian/Util.pm#L116
- @debian_package_name_regex ||= %r{\A[a-z0-9][-+\.a-z0-9]+\z}.freeze
- end
-
- def debian_version_regex
- # See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n205
- @debian_version_regex ||= %r{
- \A(?:
- (?:([0-9]{1,9}):)? (?# epoch)
- ([0-9][0-9a-z\.+~]*) (?# version)
- (-[0-9a-z\.+~]+){0,14} (?# -revision)
- (?<!-)
- )\z}xi.freeze
- end
-
- def debian_architecture_regex
- # See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/arch.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n43
- # But we limit to lower case
- @debian_architecture_regex ||= %r{\A#{::Packages::Debian::ARCHITECTURE_REGEX}\z}o.freeze
- end
-
- def debian_distribution_regex
- @debian_distribution_regex ||= %r{\A#{::Packages::Debian::DISTRIBUTION_REGEX}\z}io.freeze
- end
-
- def debian_component_regex
- @debian_component_regex ||= %r{\A#{::Packages::Debian::COMPONENT_REGEX}\z}o.freeze
- end
-
- def debian_direct_upload_filename_regex
- @debian_direct_upload_filename_regex ||= %r{\A.*\.(deb|udeb|ddeb)\z}o.freeze
- end
-
- def helm_channel_regex
- @helm_channel_regex ||= %r{\A([a-zA-Z0-9](\.|-|_)?){1,255}(?<!\.|-|_)\z}.freeze
- end
-
- def helm_package_regex
- @helm_package_regex ||= %r{#{helm_channel_regex}}.freeze
- end
-
- def helm_version_regex
- # identical to semver_regex, with optional preceding 'v'
- @helm_version_regex ||= Regexp.new("\\Av?#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
- end
-
- def unbounded_semver_regex
- # See the official regex: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
-
- # The order of the alternatives in <prerelease> are intentionally
- # reordered to be greedy. Without this change, the unbounded regex would
- # only partially match "v0.0.0-20201230123456-abcdefabcdef".
- @unbounded_semver_regex ||= /
- #{_semver_major_minor_patch_regex}#{_semver_prerelease_build_regex}
- /x.freeze
- end
-
- def semver_regex
- @semver_regex ||= Regexp.new("\\A#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options).freeze
- end
-
- def semver_regex_message
- 'should follow SemVer: https://semver.org'
- end
-
- # These partial semver regexes are intended for use in composing other
- # regexes rather than being used alone.
- def _semver_major_minor_patch_regex
- @_semver_major_minor_patch_regex ||= /
- #{_semver_major_regex}\.#{_semver_minor_regex}\.#{_semver_patch_regex}
- /x.freeze
- end
-
- def _semver_major_regex
- @_semver_major_regex ||= /
- (?<major>0|[1-9]\d*)
- /x.freeze
- end
-
- def _semver_minor_regex
- @_semver_minor_regex ||= /
- (?<minor>0|[1-9]\d*)
- /x.freeze
- end
-
- def _semver_patch_regex
- @_semver_patch_regex ||= /
- (?<patch>0|[1-9]\d*)
- /x.freeze
- end
-
- def _semver_prerelease_build_regex
- @_semver_prerelease_build_regex ||= /
- (?:-(?<prerelease>(?:\d*[a-zA-Z-][0-9a-zA-Z-]*|[1-9]\d*|0)(?:\.(?:\d*[a-zA-Z-][0-9a-zA-Z-]*|[1-9]\d*|0))*))?
- (?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
- /x.freeze
- end
-
- def prefixed_semver_regex
- # identical to semver_regex, except starting with 'v'
- @prefixed_semver_regex ||= Regexp.new("\\Av#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
- end
-
- def go_package_regex
- # A Go package name looks like a URL but is not; it:
- # - Must not have a scheme, such as http:// or https://
- # - Must not have a port number, such as :8080 or :8443
-
- @go_package_regex ||= %r{
- \b (?# word boundary)
- (?<domain>
- [0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])? (?# first domain)
- (?:\.[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])?)* (?# inner domains)
- \.[a-z]{2,} (?# top-level domain)
- )
- (?<path>/(?:
- [-/$_.+!*'(),0-9a-z] (?# plain URL character)
- | %[0-9a-f]{2})* (?# URL encoded character)
- )? (?# path)
- \b (?# word boundary)
- }ix.freeze
- end
-
- def generic_package_version_regex
- maven_version_regex
- end
-
- def generic_package_name_regex
- maven_file_name_regex
- end
-
- def generic_package_file_name_regex
- generic_package_name_regex
- end
-
- def sha256_regex
- @sha256_regex ||= /\A[0-9a-f]{64}\z/i.freeze
- end
-
- def slack_link_regex
- @slack_link_regex ||= /<(.*[|].*)>/i.freeze
- end
-
- private
-
- def conan_name_regex
- @conan_name_regex ||= %r{[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{1,49}}.freeze
- end
- end
-
- module BulkImports
- def bulk_import_destination_namespace_path_regex
- # This regexp validates the string conforms to rules for a destination_namespace path:
- # i.e does not start with a non-alphanumeric character,
- # contains only alphanumeric characters, forward slashes, periods, and underscores,
- # does not end with a period or forward slash, and has a relative path structure
- # with no http protocol chars or leading or trailing forward slashes
- # eg 'source/full/path' or 'destination_namespace' not 'https://example.com/destination/namespace/path'
- # the regex also allows for an empty string ('') to be accepted as this is allowed in
- # a bulk_import POST request
- @bulk_import_destination_namespace_path_regex ||= %r/((\A\z)|(\A[0-9a-z]*(-_.)?[0-9a-z])(\/?[0-9a-z]*[-_.]?[0-9a-z])+\z)/i
- end
-
- def bulk_import_source_full_path_regex
- # This regexp validates the string conforms to rules for a source_full_path path:
- # i.e does not start with a non-alphanumeric character except for periods or underscores,
- # contains only alphanumeric characters, forward slashes, periods, and underscores,
- # does not end with a period or forward slash, and has a relative path structure
- # with no http protocol chars or leading or trailing forward slashes
- # eg 'source/full/path' or 'destination_namespace' not 'https://example.com/source/full/path'
- @bulk_import_source_full_path_regex ||= %r/\A([.]?)[^\W](\/?([-_.+]*)*[0-9a-z][-_]*)+\z/i
- end
-
- def bulk_import_source_full_path_regex_message
- bulk_import_destination_namespace_path_regex_message
- end
-
- def bulk_import_destination_namespace_path_regex_message
- "must have a relative path structure " \
- "with no HTTP protocol characters, or leading or trailing forward slashes. " \
- "Path segments must not start or end with a special character, " \
- "and must not contain consecutive special characters."
- end
- end
-
extend self
- extend Packages
extend BulkImports
+ extend MergeRequests
+ extend Packages
def group_path_regex
# This regexp validates the string conforms to rules for a group slug:
@@ -598,10 +296,6 @@ module Gitlab
@utc_date_regex ||= /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/.freeze
end
- def merge_request_draft
- /\A(?i)(\[draft\]|\(draft\)|draft:)/
- end
-
def issue
@issue ||= /(?<issue>\d+)(?<format>\+s{,1})?(?=\W|\z)/
end
@@ -610,10 +304,6 @@ module Gitlab
@work_item ||= /(?<work_item>\d+)(?<format>\+s{,1})?(?=\W|\z)/
end
- def merge_request
- @merge_request ||= /(?<merge_request>\d+)(?<format>\+s{,1})?/
- end
-
def base64_regex
@base64_regex ||= %r{(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?}.freeze
end
@@ -636,10 +326,6 @@ module Gitlab
@x509_subject_key_identifier_regex ||= /\A(?:\h{2}:)*\h{2}\z/.freeze
end
- def ml_model_version_regex
- maven_version_regex
- end
-
def ml_model_name_regex
package_name_regex
end
diff --git a/lib/gitlab/regex/bulk_imports.rb b/lib/gitlab/regex/bulk_imports.rb
new file mode 100644
index 00000000000..e9ec24b831f
--- /dev/null
+++ b/lib/gitlab/regex/bulk_imports.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Regex
+ module BulkImports
+ def bulk_import_destination_namespace_path_regex
+ # This regexp validates the string conforms to rules for a destination_namespace path:
+ # i.e does not start with a non-alphanumeric character,
+ # contains only alphanumeric characters, forward slashes, periods, and underscores,
+ # does not end with a period or forward slash, and has a relative path structure
+ # with no http protocol chars or leading or trailing forward slashes
+ # eg 'source/full/path' or 'destination_namespace' not 'https://example.com/destination/namespace/path'
+ # the regex also allows for an empty string ('') to be accepted as this is allowed in
+ # a bulk_import POST request
+ @bulk_import_destination_namespace_path_regex ||= %r/((\A\z)|(\A[0-9a-z]*(-_.)?[0-9a-z])(\/?[0-9a-z]*[-_.]?[0-9a-z])+\z)/i
+ end
+
+ def bulk_import_source_full_path_regex
+ # This regexp validates the string conforms to rules for a source_full_path path:
+ # i.e does not start with a non-alphanumeric character except for periods or underscores,
+ # contains only alphanumeric characters, forward slashes, periods, and underscores,
+ # does not end with a period or forward slash, and has a relative path structure
+ # with no http protocol chars or leading or trailing forward slashes
+ # eg 'source/full/path' or 'destination_namespace' not 'https://example.com/source/full/path'
+ @bulk_import_source_full_path_regex ||= %r/\A([.]?)[^\W](\/?([-_.+]*)*[0-9a-z][-_]*)+\z/i
+ end
+
+ def bulk_import_source_full_path_regex_message
+ bulk_import_destination_namespace_path_regex_message
+ end
+
+ def bulk_import_destination_namespace_path_regex_message
+ "must have a relative path structure " \
+ "with no HTTP protocol characters, or leading or trailing forward slashes. " \
+ "Path segments must not start or end with a special character, " \
+ "and must not contain consecutive special characters."
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/regex/merge_requests.rb b/lib/gitlab/regex/merge_requests.rb
new file mode 100644
index 00000000000..a3212d2c7d6
--- /dev/null
+++ b/lib/gitlab/regex/merge_requests.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Regex
+ module MergeRequests
+ def merge_request
+ @merge_request ||= /(?<merge_request>\d+)(?<format>\+s{,1})?/
+ end
+
+ def merge_request_draft
+ /\A(?i)(\[draft\]|\(draft\)|draft:)/
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/regex/packages.rb b/lib/gitlab/regex/packages.rb
new file mode 100644
index 00000000000..107f2070801
--- /dev/null
+++ b/lib/gitlab/regex/packages.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Regex
+ module Packages
+ CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
+ CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
+
+ PYPI_NORMALIZED_NAME_REGEX_STRING = '[-_.]+'
+
+ # see https://github.com/apache/maven/blob/c1dfb947b509e195c75d4275a113598cf3063c3e/maven-artifact/src/main/java/org/apache/maven/artifact/Artifact.java#L46
+ MAVEN_SNAPSHOT_DYNAMIC_PARTS = /\A.{0,1000}(-\d{8}\.\d{6}-\d+).{0,1000}\z/.freeze
+
+ API_PATH_REGEX = %r{^/api/v\d+/(projects/[^/]+/|groups?/[^/]+/-/)?packages/[A-Za-z]+}.freeze
+
+ def conan_package_reference_regex
+ @conan_package_reference_regex ||= %r{\A[A-Za-z0-9]+\z}.freeze
+ end
+
+ def conan_revision_regex
+ @conan_revision_regex ||= %r{\A0\z}.freeze
+ end
+
+ def conan_recipe_user_channel_regex
+ %r{\A(_|#{conan_name_regex})\z}.freeze
+ end
+
+ def conan_recipe_component_regex
+ # https://docs.conan.io/en/latest/reference/conanfile/attributes.html#name
+ @conan_recipe_component_regex ||= %r{\A#{conan_name_regex}\z}.freeze
+ end
+
+ def composer_package_version_regex
+ # see https://github.com/composer/semver/blob/31f3ea725711245195f62e54ffa402d8ef2fdba9/src/VersionParser.php#L215
+ @composer_package_version_regex ||= %r{\Av?((\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?)?\z}.freeze
+ end
+
+ def composer_dev_version_regex
+ @composer_dev_version_regex ||= %r{(^dev-)|(-dev$)}.freeze
+ end
+
+ def package_name_regex
+ @package_name_regex ||=
+ %r{
+ \A\@?
+ (?> # atomic group to prevent backtracking
+ (([\w\-\.\+]*)\/)*([\w\-\.]+)
+ )
+ @?
+ (?> # atomic group to prevent backtracking
+ (([\w\-\.\+]*)\/)*([\w\-\.]*)
+ )
+ \z
+ }x.freeze
+ end
+
+ def maven_file_name_regex
+ @maven_file_name_regex ||= %r{\A[A-Za-z0-9\.\_\-\+]+\z}.freeze
+ end
+
+ def maven_path_regex
+ @maven_path_regex ||= %r{\A\@?(([\w\-\.]*)/)*([\w\-\.\+]*)\z}.freeze
+ end
+
+ def maven_app_name_regex
+ @maven_app_name_regex ||= /\A[\w\-\.]+\z/.freeze
+ end
+
+ def maven_version_regex
+ @maven_version_regex ||= /\A(?!.*\.\.)[\w+.-]+\z/.freeze
+ end
+
+ def maven_app_group_regex
+ maven_app_name_regex
+ end
+
+ def npm_package_name_regex
+ @npm_package_name_regex ||= %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9]+\z}o
+ end
+
+ def npm_package_name_regex_message
+ 'should be a valid NPM package name: https://github.com/npm/validate-npm-package-name#naming-rules.'
+ end
+
+ def nuget_package_name_regex
+ @nuget_package_name_regex ||= %r{\A[-+\.\_a-zA-Z0-9]+\z}.freeze
+ end
+
+ def nuget_version_regex
+ @nuget_version_regex ||= /
+ \A#{_semver_major_regex}
+ \.#{_semver_minor_regex}
+ (\.#{_semver_patch_regex})?
+ (\.\d*)?
+ #{_semver_prerelease_build_regex}\z
+ /x.freeze
+ end
+
+ def terraform_module_package_name_regex
+ @terraform_module_package_name_regex ||= %r{\A[-a-z0-9]+\/[-a-z0-9]+\z}.freeze
+ end
+
+ def pypi_version_regex
+ # See the official regex: https://github.com/pypa/packaging/blob/16.7/packaging/version.py#L159
+
+ @pypi_version_regex ||= %r{
+ \A(?:
+ v?
+ (?:([0-9]+)!)? (?# epoch)
+ ([0-9]+(?:\.[0-9]+)*) (?# release segment)
+ ([-_\.]?((a|b|c|rc|alpha|beta|pre|preview))[-_\.]?([0-9]+)?)? (?# pre-release)
+ ((?:-([0-9]+))|(?:[-_\.]?(post|rev|r)[-_\.]?([0-9]+)?))? (?# post release)
+ ([-_\.]?(dev)[-_\.]?([0-9]+)?)? (?# dev release)
+ (?:\+([a-z0-9]+(?:[-_\.][a-z0-9]+)*))? (?# local version)
+ )\z}xi.freeze
+ end
+
+ def debian_package_name_regex
+ # See official parser
+ # https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n122
+ # @debian_package_name_regex ||= %r{\A[a-z0-9][-+\._a-z0-9]*\z}i.freeze
+ # But we prefer a more strict version from Lintian
+ # https://salsa.debian.org/lintian/lintian/-/blob/5080c0068ffc4a9ddee92022a91d0c2ff53e56d1/lib/Lintian/Util.pm#L116
+ @debian_package_name_regex ||= %r{\A[a-z0-9][-+\.a-z0-9]+\z}.freeze
+ end
+
+ def debian_version_regex
+ # See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n205
+ @debian_version_regex ||= %r{
+ \A(?:
+ (?:([0-9]{1,9}):)? (?# epoch)
+ ([0-9][0-9a-z\.+~]*) (?# version)
+ (-[0-9a-z\.+~]+){0,14} (?# -revision)
+ (?<!-)
+ )\z}xi.freeze
+ end
+
+ def debian_architecture_regex
+ # See official parser: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/arch.c?id=9e0c88ec09475f4d1addde9cdba1ad7849720356#n43
+ # But we limit to lower case
+ @debian_architecture_regex ||= %r{\A#{::Packages::Debian::ARCHITECTURE_REGEX}\z}o.freeze
+ end
+
+ def debian_distribution_regex
+ @debian_distribution_regex ||= %r{\A#{::Packages::Debian::DISTRIBUTION_REGEX}\z}io.freeze
+ end
+
+ def debian_component_regex
+ @debian_component_regex ||= %r{\A#{::Packages::Debian::COMPONENT_REGEX}\z}o.freeze
+ end
+
+ def debian_direct_upload_filename_regex
+ @debian_direct_upload_filename_regex ||= %r{\A.*\.(deb|udeb|ddeb)\z}o.freeze
+ end
+
+ def helm_channel_regex
+ @helm_channel_regex ||= %r{\A([a-zA-Z0-9](\.|-|_)?){1,255}(?<!\.|-|_)\z}.freeze
+ end
+
+ def helm_package_regex
+ @helm_package_regex ||= %r{#{helm_channel_regex}}.freeze
+ end
+
+ def helm_version_regex
+ # identical to semver_regex, with optional preceding 'v'
+ @helm_version_regex ||= Regexp.new("\\Av?#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
+ end
+
+ def unbounded_semver_regex
+ # See the official regex: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+
+ # The order of the alternatives in <prerelease> are intentionally
+ # reordered to be greedy. Without this change, the unbounded regex would
+ # only partially match "v0.0.0-20201230123456-abcdefabcdef".
+ @unbounded_semver_regex ||= /
+ #{_semver_major_minor_patch_regex}#{_semver_prerelease_build_regex}
+ /x.freeze
+ end
+
+ def semver_regex
+ @semver_regex ||= Regexp.new("\\A#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options).freeze
+ end
+
+ def semver_regex_message
+ 'should follow SemVer: https://semver.org'
+ end
+
+ # These partial semver regexes are intended for use in composing other
+ # regexes rather than being used alone.
+ def _semver_major_minor_patch_regex
+ @_semver_major_minor_patch_regex ||= /
+ #{_semver_major_regex}\.#{_semver_minor_regex}\.#{_semver_patch_regex}
+ /x.freeze
+ end
+
+ def _semver_major_regex
+ @_semver_major_regex ||= /
+ (?<major>0|[1-9]\d*)
+ /x.freeze
+ end
+
+ def _semver_minor_regex
+ @_semver_minor_regex ||= /
+ (?<minor>0|[1-9]\d*)
+ /x.freeze
+ end
+
+ def _semver_patch_regex
+ @_semver_patch_regex ||= /
+ (?<patch>0|[1-9]\d*)
+ /x.freeze
+ end
+
+ def _semver_prerelease_build_regex
+ @_semver_prerelease_build_regex ||= /
+ (?:-(?<prerelease>(?:\d*[a-zA-Z-][0-9a-zA-Z-]*|[1-9]\d*|0)(?:\.(?:\d*[a-zA-Z-][0-9a-zA-Z-]*|[1-9]\d*|0))*))?
+ (?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
+ /x.freeze
+ end
+
+ def prefixed_semver_regex
+ # identical to semver_regex, except starting with 'v'
+ @prefixed_semver_regex ||= Regexp.new("\\Av#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
+ end
+
+ def go_package_regex
+ # A Go package name looks like a URL but is not; it:
+ # - Must not have a scheme, such as http:// or https://
+ # - Must not have a port number, such as :8080 or :8443
+
+ @go_package_regex ||= %r{
+ \b (?# word boundary)
+ (?<domain>
+ [0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])? (?# first domain)
+ (?:\.[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])?)* (?# inner domains)
+ \.[a-z]{2,} (?# top-level domain)
+ )
+ (?<path>/(?:
+ [-/$_.+!*'(),0-9a-z] (?# plain URL character)
+ | %[0-9a-f]{2})* (?# URL encoded character)
+ )? (?# path)
+ \b (?# word boundary)
+ }ix.freeze
+ end
+
+ def generic_package_version_regex
+ maven_version_regex
+ end
+
+ def generic_package_name_regex
+ maven_file_name_regex
+ end
+
+ def generic_package_file_name_regex
+ generic_package_name_regex
+ end
+
+ def sha256_regex
+ @sha256_regex ||= /\A[0-9a-f]{64}\z/i.freeze
+ end
+
+ def slack_link_regex
+ @slack_link_regex ||= /<(.*[|].*)>/i.freeze
+ end
+
+ private
+
+ def conan_name_regex
+ @conan_name_regex ||= %r{[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{1,49}}.freeze
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c1a96a97fe2..4b557fd1aef 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6982,7 +6982,7 @@ msgstr ""
msgid "Available group runners: %{runners}"
msgstr ""
-msgid "Available on-demand"
+msgid "Available on demand"
msgstr ""
msgid "Avatar for %{assigneeName}"
@@ -16586,6 +16586,9 @@ msgstr ""
msgid "DiscordService|Discord Notifications"
msgstr ""
+msgid "DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)"
+msgstr ""
+
msgid "DiscordService|Send notifications about project events to a Discord channel."
msgstr ""
@@ -19827,9 +19830,15 @@ msgstr ""
msgid "For a faster browsing experience, some files are collapsed by default."
msgstr ""
+msgid "For additional information, review your %{link_to} or contact your %{project_or_group} owner."
+msgstr ""
+
msgid "For additional information, review your %{link_to} or contact your group owner."
msgstr ""
+msgid "For additional information, review your %{project_or_group} membership: %{url} or contact your %{project_or_group} owner."
+msgstr ""
+
msgid "For additional information, review your group membership: %{link_to} or contact your group owner."
msgstr ""
@@ -42120,6 +42129,9 @@ msgstr ""
msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
+msgid "SecurityReports|A comment is required when dismissing."
+msgstr ""
+
msgid "SecurityReports|Activity"
msgstr ""
@@ -42444,6 +42456,9 @@ msgstr ""
msgid "SecurityReports|These vulnerabilities were detected in external sources. They are not necessarily tied to your GitLab project. For example, running containers, URLs, and so on."
msgstr ""
+msgid "SecurityReports|This selection is required."
+msgstr ""
+
msgid "SecurityReports|To widen your search, change or remove filters above"
msgstr ""
@@ -53880,6 +53895,15 @@ msgstr ""
msgid "Your membership in %{group} no longer expires."
msgstr ""
+msgid "Your membership in %{link_to} %{project_or_group_name} will expire in %{days_formatted}."
+msgstr ""
+
+msgid "Your membership in %{project_or_group} %{project_or_group_name} will expire in %{days_formatted}."
+msgstr ""
+
+msgid "Your membership will expire in %{days_to_expire} days"
+msgstr ""
+
msgid "Your name"
msgstr ""
diff --git a/qa/gdk/gdk.yml b/qa/gdk/gdk.yml
index 649ac9a60c6..36bf901033c 100644
--- a/qa/gdk/gdk.yml
+++ b/qa/gdk/gdk.yml
@@ -18,6 +18,7 @@ gitlab:
rails:
bootsnap: false
hostname: gdk.test
+ application_settings_cache_seconds: 0
gitlab_k8s_agent:
enabled: false
gitlab_pages:
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
index 78604f08390..7233318bb13 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
@@ -44,7 +44,12 @@ module QA
it(
'allows enforcing 2FA via UI and logging in with 2FA',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347931'
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347931',
+ quarantine: {
+ type: :bug,
+ only: { condition: -> { QA::Runtime::Env.super_sidebar_enabled? } },
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409336'
+ }
) do
enforce_two_factor_authentication_on_group(group)
diff --git a/rubocop/cop/rspec/before_all.rb b/rubocop/cop/rspec/before_all.rb
new file mode 100644
index 00000000000..3fbb9447bc6
--- /dev/null
+++ b/rubocop/cop/rspec/before_all.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'rubocop-rspec'
+
+module Rubocop
+ module Cop
+ module RSpec
+ # This cop checks for `before(:all) in RSpec tests`
+ #
+ # @example
+ #
+ # bad
+ #
+ # before(:all) do
+ # project.repository.add_tag(user, 'v1.2.3', 'master')
+ # end
+ #
+ # good
+ #
+ # before_all do
+ # project.repository.add_tag(user, 'v1.2.3', 'master')
+ # end
+ #
+ class BeforeAll < RuboCop::Cop::Base
+ extend RuboCop::Cop::AutoCorrector
+
+ MSG = "Prefer using `before_all` over `before(:all)`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#common-test-setup"
+
+ RESTRICT_ON_SEND = %i[before].freeze
+
+ def_node_matcher :before_all_block?, <<~PATTERN
+ (send nil? :before (sym :all) ...)
+ PATTERN
+
+ def on_send(node)
+ return unless before_all_block?(node)
+
+ add_offense(node) do |corrector|
+ replacement = 'before_all'
+ corrector.replace(node.source_range, replacement)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index 8c1cdf784aa..49851c82cc5 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -379,6 +379,26 @@ RSpec.describe Projects::Settings::IntegrationsController, feature_category: :in
end
end
end
+
+ context 'with chat notification integration which masks channel params' do
+ let_it_be(:integration) do
+ create(:discord_integration, project: project, note_channel: 'https://discord.com/api/webhook/note')
+ end
+
+ let(:message) { 'Discord Notifications settings saved and active.' }
+
+ it_behaves_like 'integration update'
+
+ context 'with masked channel param' do
+ let(:integration_params) { { active: true, note_channel: '************' } }
+
+ it_behaves_like 'integration update'
+
+ it 'does not update the channel' do
+ expect(integration.reload.note_channel).to eq('https://discord.com/api/webhook/note')
+ end
+ end
+ end
end
describe 'as JSON' do
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index a89edc19cc7..0fa54e72b7c 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -254,6 +254,13 @@ FactoryBot.define do
active { false }
end
+ factory :discord_integration, class: 'Integrations::Discord' do
+ chat_notification
+ project
+ active { true }
+ type { 'Integrations::Discord' }
+ end
+
factory :mattermost_integration, class: 'Integrations::Mattermost' do
chat_notification
project
diff --git a/spec/factories/ml/model_versions.rb b/spec/factories/ml/model_versions.rb
index 61d42fa5b5c..456d1b1e913 100644
--- a/spec/factories/ml/model_versions.rb
+++ b/spec/factories/ml/model_versions.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :ml_model_versions, class: '::Ml::ModelVersion' do
- sequence(:version) { |n| "version#{n}" }
+ sequence(:version) { |n| "1.0.#{n}-alpha+test" }
model { association :ml_models }
project { model.project }
diff --git a/spec/frontend/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
index ffee902e161..59b9da141b8 100644
--- a/spec/frontend/service_desk/components/service_desk_list_app_spec.js
+++ b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { cloneDeep } from 'lodash';
import * as Sentry from '@sentry/browser';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,8 +10,8 @@ import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { STATUS_CLOSED, STATUS_OPEN } from '~/service_desk/constants';
-import getServiceDeskIssuesQuery from '~/service_desk/queries/get_service_desk_issues.query.graphql';
-import getServiceDeskIssuesCountsQuery from '~/service_desk/queries/get_service_desk_issues_counts.query.graphql';
+import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCountsQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
import InfoBanner from '~/service_desk/components/info_banner.vue';
import {
@@ -39,6 +40,8 @@ describe('ServiceDeskListApp', () => {
releasesPath: 'releases/path',
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
hasIterationsFeature: true,
+ hasIssueWeightsFeature: true,
+ hasIssuableHealthStatusFeature: true,
groupPath: 'group/path',
emptyStateSvgPath: 'empty-state.svg',
isProject: true,
@@ -48,7 +51,12 @@ describe('ServiceDeskListApp', () => {
hasAnyIssues: true,
};
- const defaultQueryResponse = getServiceDeskIssuesQueryResponse;
+ let defaultQueryResponse = getServiceDeskIssuesQueryResponse;
+ if (IS_EE) {
+ defaultQueryResponse = cloneDeep(getServiceDeskIssuesQueryResponse);
+ defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null;
+ defaultQueryResponse.data.project.issues.nodes[0].weight = 5;
+ }
const mockServiceDeskIssuesQueryResponseHandler = jest
.fn()
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 83d7d298e17..a486d13a856 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,6 +1,8 @@
import hljs from 'highlight.js/lib/core';
import Vue from 'vue';
import VueRouter from 'vue-router';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
@@ -16,6 +18,7 @@ import {
CODEOWNERS_LANGUAGE,
SVELTE_LANGUAGE,
} from '~/vue_shared/components/source_viewer/constants';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
@@ -26,6 +29,7 @@ jest.mock('highlight.js/lib/core');
jest.mock('~/vue_shared/components/source_viewer/plugins/index');
Vue.use(VueRouter);
const router = new VueRouter();
+const mockAxios = new MockAdapter(axios);
const generateContent = (content, totalLines = 1, delimiter = '\n') => {
let generatedContent = '';
@@ -72,6 +76,42 @@ describe('Source Viewer component', () => {
return createComponent();
});
+ describe('Displaying LFS blob', () => {
+ const rawPath = '/org/project/-/raw/file.xml';
+ const externalStorageUrl = 'http://127.0.0.1:9000/lfs-objects/91/12/1341234';
+ const rawTextBlob = 'This is the external content';
+ const blob = {
+ storedExternally: true,
+ externalStorage: 'lfs',
+ simpleViewer: { fileType: 'text' },
+ rawPath,
+ };
+
+ afterEach(() => {
+ mockAxios.reset();
+ });
+
+ it('Uses externalStorageUrl to fetch content if present', async () => {
+ mockAxios.onGet(externalStorageUrl).replyOnce(HTTP_STATUS_OK, rawTextBlob);
+
+ await createComponent({ ...blob, externalStorageUrl });
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toBe(externalStorageUrl);
+ expect(wrapper.vm.$data.content).toBe(rawTextBlob);
+ });
+
+ it('Falls back to rawPath to fetch content', async () => {
+ mockAxios.onGet(rawPath).replyOnce(HTTP_STATUS_OK, rawTextBlob);
+
+ await createComponent(blob);
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toBe(rawPath);
+ expect(wrapper.vm.$data.content).toBe(rawTextBlob);
+ });
+ });
+
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: language };
diff --git a/spec/helpers/ci/variables_helper_spec.rb b/spec/helpers/ci/variables_helper_spec.rb
index 9c3236ace72..13970dd95b4 100644
--- a/spec/helpers/ci/variables_helper_spec.rb
+++ b/spec/helpers/ci/variables_helper_spec.rb
@@ -3,9 +3,71 @@
require 'spec_helper'
RSpec.describe Ci::VariablesHelper, feature_category: :secrets_management do
+ describe '#create_deploy_token_path' do
+ let_it_be(:group) { build_stubbed(:group) }
+ let_it_be(:project) { build_stubbed(:project) }
+
+ it 'returns the project deploy token path' do
+ expect(helper.create_deploy_token_path(project)).to eq(
+ create_deploy_token_project_settings_repository_path(project, {})
+ )
+ end
+
+ it 'returns the group deploy token path' do
+ expect(helper.create_deploy_token_path(group)).to eq(
+ create_deploy_token_group_settings_repository_path(group, {})
+ )
+ end
+ end
+
+ describe '#ci_variable_protected?' do
+ let(:variable) { build_stubbed(:ci_variable, key: 'test_key', value: 'test_value', protected: true) }
+
+ context 'when variable is provided and only_key_value is false' do
+ it 'expect ci_variable_protected? to return true' do
+ expect(helper.ci_variable_protected?(variable, false)).to eq(true)
+ end
+ end
+
+ context 'when variable is not provided / provided and only_key_value is true' do
+ it 'is equal to the value of ci_variable_protected_by_default?' do
+ expect(helper.ci_variable_protected?(nil, true)).to eq(
+ helper.ci_variable_protected_by_default?
+ )
+
+ expect(helper.ci_variable_protected?(variable, true)).to eq(
+ helper.ci_variable_protected_by_default?
+ )
+ end
+ end
+ end
+
+ describe '#ci_variable_masked?' do
+ let(:variable) { build_stubbed(:ci_variable, key: 'test_key', value: 'test_value', masked: true) }
+
+ context 'when variable is provided and only_key_value is false' do
+ it 'expect ci_variable_masked? to return true' do
+ expect(helper.ci_variable_masked?(variable, false)).to eq(true)
+ end
+ end
+
+ context 'when variable is not provided / provided and only_key_value is true' do
+ it 'expect ci_variable_masked? to return false' do
+ expect(helper.ci_variable_masked?(nil, true)).to eq(false)
+ expect(helper.ci_variable_masked?(variable, true)).to eq(false)
+ end
+ end
+ end
+
describe '#ci_variable_maskable_raw_regex' do
it 'converts to a javascript regex' do
expect(helper.ci_variable_maskable_raw_regex).to eq("^\\S{8,}$")
end
end
+
+ describe '#ci_variable_maskable_regex' do
+ it 'converts to a javascript regex' do
+ expect(helper.ci_variable_maskable_regex).to eq("^[a-zA-Z0-9_+=/@:.~-]{8,}$")
+ end
+ end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 092d3c07716..c91b99caba2 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -832,6 +832,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to match('1.2.3') }
it { is_expected.to match('1.2.3-beta') }
it { is_expected.to match('1.2.3-alpha.3') }
+ it { is_expected.to match('1.2.3-alpha.3+abcd') }
it { is_expected.not_to match('1') }
it { is_expected.not_to match('1.2') }
it { is_expected.not_to match('1./2.3') }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 629dfdaf55e..976fe214c95 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -2056,6 +2056,68 @@ RSpec.describe Notify do
end
end
+ describe 'membership about to expire' do
+ context "with group membership" do
+ let_it_be(:group_member) { create(:group_member, source: group, expires_at: 7.days.from_now) }
+
+ subject { described_class.member_about_to_expire_email("Namespace", group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
+ it 'contains all the useful information' do
+ is_expected.to deliver_to group_member.user.email
+ is_expected.to have_subject "Your membership will expire in 7 days"
+ is_expected.to have_body_text "group will expire in 7 days."
+ is_expected.to have_body_text group_url(group)
+ is_expected.to have_body_text group_group_members_url(group)
+ end
+ end
+
+ context "with project membership" do
+ let_it_be(:project_member) { create(:project_member, source: project, expires_at: 7.days.from_now) }
+
+ subject { described_class.member_about_to_expire_email('Project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
+ it 'contains all the useful information' do
+ is_expected.to deliver_to project_member.user.email
+ is_expected.to have_subject "Your membership will expire in 7 days"
+ is_expected.to have_body_text "project will expire in 7 days."
+ is_expected.to have_body_text project_url(project)
+ is_expected.to have_body_text project_project_members_url(project)
+ end
+ end
+
+ context "with expired membership" do
+ let_it_be(:project_member) { create(:project_member, source: project, expires_at: Date.today) }
+
+ subject { described_class.member_about_to_expire_email('Project', project_member.id) }
+
+ it 'not deliver expiry email' do
+ should_not_email_anyone
+ end
+ end
+
+ context "with expiry notified membership" do
+ let_it_be(:project_member) { create(:project_member, source: project, expires_at: 7.days.from_now, expiry_notified_at: Date.today) }
+
+ subject { described_class.member_about_to_expire_email('Project', project_member.id) }
+
+ it 'not deliver expiry email' do
+ should_not_email_anyone
+ end
+ end
+ end
+
describe 'admin notification' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
diff --git a/spec/migrations/20230714015909_add_index_for_member_expiring_query_spec.rb b/spec/migrations/20230714015909_add_index_for_member_expiring_query_spec.rb
new file mode 100644
index 00000000000..524354ecc9a
--- /dev/null
+++ b/spec/migrations/20230714015909_add_index_for_member_expiring_query_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddIndexForMemberExpiringQuery, :migration, feature_category: :groups_and_projects do
+ let(:index_name) { 'index_members_on_expiring_at_access_level_id' }
+
+ it 'correctly migrates up and down' do
+ expect(subject).not_to be_index_exists_by_name(:members, index_name)
+
+ migrate!
+
+ expect(subject).to be_index_exists_by_name(:members, index_name)
+ end
+end
diff --git a/spec/migrations/add_expiry_notified_at_to_member_spec.rb b/spec/migrations/add_expiry_notified_at_to_member_spec.rb
new file mode 100644
index 00000000000..30eaf06529e
--- /dev/null
+++ b/spec/migrations/add_expiry_notified_at_to_member_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddExpiryNotifiedAtToMember, feature_category: :system_access do
+ let(:members) { table(:members) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(members.column_names).not_to include('expiry_notified_at')
+ }
+
+ migration.after -> {
+ members.reset_column_information
+ expect(members.column_names).to include('expiry_notified_at')
+ }
+ end
+ end
+end
diff --git a/spec/models/integrations/discord_spec.rb b/spec/models/integrations/discord_spec.rb
index 42ea4a287fe..7ab7308ac1c 100644
--- a/spec/models/integrations/discord_spec.rb
+++ b/spec/models/integrations/discord_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Integrations::Discord do
+RSpec.describe Integrations::Discord, feature_category: :integrations do
it_behaves_like "chat integration", "Discord notifications" do
let(:client) { Discordrb::Webhooks::Client }
let(:client_arguments) { { url: webhook_url } }
@@ -20,6 +20,26 @@ RSpec.describe Integrations::Discord do
end
end
+ describe 'validations' do
+ let_it_be(:project) { create(:project) }
+
+ subject { integration }
+
+ describe 'only allows one channel on events' do
+ context 'when given more than one channel' do
+ let(:integration) { build(:discord_integration, project: project, note_channel: 'webhook1,webhook2') }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when given one channel' do
+ let(:integration) { build(:discord_integration, project: project, note_channel: 'webhook1') }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
describe '#execute' do
include StubRequests
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index d21edea9751..f8aaae3edad 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -351,6 +351,19 @@ RSpec.describe Member, feature_category: :groups_and_projects do
it { is_expected.to include(expiring_tomorrow, not_expiring) }
end
+ describe '.expiring_and_not_notified' do
+ let_it_be(:expiring_in_5_days) { create(:group_member, expires_at: 5.days.from_now) }
+ let_it_be(:expiring_in_5_days_with_notified) { create(:group_member, expires_at: 5.days.from_now, expiry_notified_at: Date.today) }
+ let_it_be(:expiring_in_7_days) { create(:group_member, expires_at: 7.days.from_now) }
+ let_it_be(:expiring_in_10_days) { create(:group_member, expires_at: 10.days.from_now) }
+ let_it_be(:not_expiring) { create(:group_member) }
+
+ subject { described_class.expiring_and_not_notified(7.days.from_now.to_date) }
+
+ it { is_expected.not_to include(expiring_in_5_days_with_notified, expiring_in_10_days, not_expiring) }
+ it { is_expected.to include(expiring_in_5_days, expiring_in_7_days) }
+ 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) }
diff --git a/spec/models/ml/model_version_spec.rb b/spec/models/ml/model_version_spec.rb
index 8a57a7bc378..4bb272fef5d 100644
--- a/spec/models/ml/model_version_spec.rb
+++ b/spec/models/ml/model_version_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
end
describe 'validation' do
- let_it_be(:valid_version) { 'valid_version' }
+ let_it_be(:valid_version) { '1.0.0' }
let_it_be(:valid_package) do
build_stubbed(:ml_model_package, project: base_project, version: valid_version, name: model1.name)
end
@@ -95,7 +95,7 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
end
describe '#find_or_create!' do
- let_it_be(:existing_model_version) { create(:ml_model_versions, model: model1, version: 'abc') }
+ let_it_be(:existing_model_version) { create(:ml_model_versions, model: model1, version: '1.0.0') }
let(:version) { existing_model_version.version }
let(:package) { nil }
@@ -110,7 +110,7 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
end
context 'if model version does not exist' do
- let(:version) { 'new_version' }
+ let(:version) { '2.0.0' }
let(:package) { create(:ml_model_package, project: model1.project, name: model1.name, version: version) }
it 'creates another model version', :aggregate_failures do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index ed0cec46a42..c7b7131a600 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -238,7 +238,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
]
end
- before(:all) do
+ before_all do
project.update!(group: group)
end
diff --git a/spec/rubocop/cop/rspec/before_all_spec.rb b/spec/rubocop/cop/rspec/before_all_spec.rb
new file mode 100644
index 00000000000..5cf22bc4093
--- /dev/null
+++ b/spec/rubocop/cop/rspec/before_all_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/rspec/before_all'
+
+RSpec.describe Rubocop::Cop::RSpec::BeforeAll, feature_category: :tooling do
+ context 'when using before(:all)' do
+ let(:source) do
+ <<~SRC
+ before(:all) do
+ ^^^^^^^^^^^^ Prefer using `before_all` over `before(:all)`. [...]
+ create_table_structure
+ end
+ SRC
+ end
+
+ let(:corrected_source) do
+ <<~SRC
+ before_all do
+ create_table_structure
+ end
+ SRC
+ end
+
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(source)
+
+ expect_correction(corrected_source)
+ end
+ end
+
+ context 'when using before_all' do
+ let(:source) do
+ <<~SRC
+ before_all do
+ create_table_structure
+ end
+ SRC
+ end
+
+ it 'does not register an offense' do
+ expect_no_offenses(source)
+ end
+ end
+
+ context 'when using before(:each)' do
+ let(:source) do
+ <<~SRC
+ before(:each) do
+ create_table_structure
+ end
+ SRC
+ end
+
+ it 'does not register an offense' do
+ expect_no_offenses(source)
+ end
+ end
+
+ context 'when using before' do
+ let(:source) do
+ <<~SRC
+ before do
+ create_table_structure
+ end
+ SRC
+ end
+
+ it 'does not register an offense' do
+ expect_no_offenses(source)
+ end
+ end
+end
diff --git a/spec/serializers/integrations/event_entity_spec.rb b/spec/serializers/integrations/event_entity_spec.rb
index 1b72b5d290c..a15c1bea61a 100644
--- a/spec/serializers/integrations/event_entity_spec.rb
+++ b/spec/serializers/integrations/event_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::EventEntity do
+RSpec.describe Integrations::EventEntity, feature_category: :integrations do
let(:request) { EntityRequest.new(integration: integration) }
subject { described_class.new(event, request: request, integration: integration).as_json }
@@ -38,5 +38,24 @@ RSpec.describe Integrations::EventEntity do
expect(subject[:field][:placeholder]).to eq('#general, #development')
end
end
+
+ context 'with integration with fields when channels are masked' do
+ let(:integration) { create(:integrations_slack, note_events: false, note_channel: 'note-channel') }
+ let(:event) { 'note' }
+
+ before do
+ allow(integration).to receive(:mask_configurable_channels?).and_return(true)
+ end
+
+ it 'exposes correct attributes' do
+ expect(subject[:description]).to eq('Trigger event for new comments.')
+ expect(subject[:name]).to eq('note_events')
+ expect(subject[:title]).to eq('Note')
+ expect(subject[:value]).to eq(false)
+ expect(subject[:field][:name]).to eq('note_channel')
+ expect(subject[:field][:value]).to eq(Integrations::BaseChatNotification::SECRET_MASK)
+ expect(subject[:field][:placeholder]).to eq('#general, #development')
+ end
+ end
end
end
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index 1c4b1abcfdb..3860543a85e 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -219,6 +219,25 @@ RSpec.describe Members::UpdateService, feature_category: :groups_and_projects do
end
end
end
+
+ context 'when project members expiration date is updated with expiry_notified_at' do
+ let_it_be(:params) { { expires_at: 20.days.from_now } }
+
+ before do
+ group_project.group.add_owner(current_user)
+ members.each do |member|
+ member.update!(expiry_notified_at: Date.today)
+ end
+ end
+
+ it "clear expiry_notified_at" do
+ subject
+
+ members.each do |member|
+ expect(member.reload.expiry_notified_at).to be_nil
+ end
+ end
+ end
end
shared_examples 'updating a group' do
@@ -250,6 +269,24 @@ RSpec.describe Members::UpdateService, feature_category: :groups_and_projects do
subject
end
end
+
+ context 'when group members expiration date is updated with expiry_notified_at' do
+ let_it_be(:params) { { expires_at: 20.days.from_now } }
+
+ before do
+ members.each do |member|
+ member.update!(expiry_notified_at: Date.today)
+ end
+ end
+
+ it "clear expiry_notified_at" do
+ subject
+
+ members.each do |member|
+ expect(member.reload.expiry_notified_at).to be_nil
+ end
+ end
+ end
end
subject { update_service.execute(members, permission: permission) }
diff --git a/spec/services/ml/find_or_create_model_version_service_spec.rb b/spec/services/ml/find_or_create_model_version_service_spec.rb
index 5c29a3430d8..1211a9b1165 100644
--- a/spec/services/ml/find_or_create_model_version_service_spec.rb
+++ b/spec/services/ml/find_or_create_model_version_service_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops d
context 'when model version does not exist' do
let(:project) { existing_version.project }
let(:name) { 'a_new_model' }
- let(:version) { 'a_new_version' }
+ let(:version) { '2.0.0' }
let(:package) { create(:ml_model_package, project: project, name: name, version: version) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 1d1dd045a09..d3cc367649f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -3380,6 +3380,27 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
+ describe '#member_about_to_expire' do
+ let_it_be(:group_member) { create(:group_member, expires_at: 7.days.from_now.to_date) }
+ let_it_be(:project_member) { create(:project_member, expires_at: 7.days.from_now.to_date) }
+
+ context "with group member" do
+ it 'emails the user that their group membership will be expired' do
+ notification.member_about_to_expire(group_member)
+
+ should_email(group_member.user)
+ end
+ end
+
+ context "with project member" do
+ it 'emails the user that their project membership will be expired' do
+ notification.member_about_to_expire(project_member)
+
+ should_email(project_member.user)
+ end
+ end
+ end
+
def create_member!
create(:project_member, user: added_user, project: project)
end
diff --git a/spec/workers/members/expiring_email_notification_worker_spec.rb b/spec/workers/members/expiring_email_notification_worker_spec.rb
new file mode 100644
index 00000000000..600a81b37b8
--- /dev/null
+++ b/spec/workers/members/expiring_email_notification_worker_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::ExpiringEmailNotificationWorker, type: :worker, feature_category: :system_access do
+ subject(:worker) { described_class.new }
+
+ let_it_be(:member) { create(:project_member, :guest, expires_at: 7.days.from_now.to_date) }
+ let_it_be(:notified_member) do
+ create(:project_member, :guest, expires_at: 7.days.from_now.to_date, expiry_notified_at: Date.today)
+ end
+
+ describe '#perform' do
+ context "with not notified member" do
+ it "notify member" do
+ expect_next_instance_of(NotificationService) do |notification_service|
+ expect(notification_service).to receive(:member_about_to_expire).with(member)
+ end
+
+ worker.perform(member.id)
+
+ expect(member.reload.expiry_notified_at).to be_present
+ end
+ end
+
+ context "with notified member" do
+ it "not notify member" do
+ expect_next_instance_of(NotificationService) do |notification_service|
+ expect(notification_service).not_to receive(:member_about_to_expire).with(notified_member)
+ end
+
+ worker.perform(notified_member.id)
+ end
+ end
+
+ context "when feature member_expiring_email_notification is disabled" do
+ before do
+ stub_feature_flags(member_expiring_email_notification: false)
+ end
+
+ it "not notify member" do
+ expect_next_instance_of(NotificationService) do |notification_service|
+ expect(notification_service).not_to receive(:member_about_to_expire).with(member)
+ end
+
+ worker.perform(member.id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/members/expiring_worker_spec.rb b/spec/workers/members/expiring_worker_spec.rb
new file mode 100644
index 00000000000..3f46548dbb3
--- /dev/null
+++ b/spec/workers/members/expiring_worker_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::ExpiringWorker, type: :worker, feature_category: :system_access do
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ let_it_be(:expiring_7_days_project_member) { create(:project_member, :guest, expires_at: 7.days.from_now) }
+ let_it_be(:expiring_7_days_group_member) { create(:group_member, :guest, expires_at: 7.days.from_now) }
+ let_it_be(:expiring_10_days_project_member) { create(:project_member, :guest, expires_at: 10.days.from_now) }
+ let_it_be(:expiring_5_days_project_member) { create(:project_member, :guest, expires_at: 5.days.from_now) }
+ let_it_be(:expiring_7_days_blocked_project_member) do
+ create(:project_member, :guest, :blocked, expires_at: 7.days.from_now)
+ end
+
+ let(:notifiy_worker) { Members::ExpiringEmailNotificationWorker }
+
+ it "notifies only active users with membership expiring in less than 7 days" do
+ expect(notifiy_worker).to receive(:perform_async).with(expiring_7_days_project_member.id)
+ expect(notifiy_worker).to receive(:perform_async).with(expiring_7_days_group_member.id)
+ expect(notifiy_worker).to receive(:perform_async).with(expiring_5_days_project_member.id)
+
+ worker.perform
+ end
+ end
+end