summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-30 18:11:31 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-30 18:11:31 +0000
commitc753fd0bf4a5cc09f69941daef0f6fe99d61f20e (patch)
tree9aee7f1af879446f226d7a67c149c817ace3f69f
parenteaec42f9e37fe51f9c53fa7079639ec9f4c40efc (diff)
downloadgitlab-ce-c753fd0bf4a5cc09f69941daef0f6fe99d61f20e.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.eslintrc.yml1
-rw-r--r--.gitignore5
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml8
-rw-r--r--.stylelintrc1
-rw-r--r--CHANGELOG.md100
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue98
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/index.js31
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js33
-rw-r--r--app/assets/javascripts/lib/utils/regexp.js8
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js15
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js4
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/users_select/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue22
-rw-r--r--app/controllers/admin/dashboard_controller.rb8
-rw-r--r--app/controllers/admin/users_controller.rb7
-rw-r--r--app/controllers/concerns/impersonation.rb19
-rw-r--r--app/controllers/health_controller.rb1
-rw-r--r--app/controllers/import/gitea_controller.rb6
-rw-r--r--app/controllers/profiles/passwords_controller.rb8
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb10
-rw-r--r--app/controllers/projects/project_members_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb5
-rw-r--r--app/controllers/uploads_controller.rb6
-rw-r--r--app/graphql/types/group_invitation_type.rb2
-rw-r--r--app/graphql/types/project_invitation_type.rb2
-rw-r--r--app/helpers/external_link_helper.rb5
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb6
-rw-r--r--app/models/project_statistics.rb1
-rw-r--r--app/models/user.rb3
-rw-r--r--app/models/user_callout.rb3
-rw-r--r--app/services/projects/destroy_service.rb11
-rw-r--r--app/views/admin/dashboard/_security_newsletter_callout.html.haml14
-rw-r--r--app/views/admin/dashboard/index.html.haml1
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml13
-rw-r--r--app/views/projects/_export.html.haml1
-rw-r--r--config/README.md170
-rw-r--r--config/application.rb12
-rw-r--r--config/initializers/7_redis.rb1
-rw-r--r--config/initializers/doorkeeper.rb5
-rw-r--r--config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb42
-rw-r--r--config/redis.cache.yml.example38
-rw-r--r--config/redis.queues.yml.example38
-rw-r--r--config/redis.shared_state.yml.example38
-rw-r--r--config/redis.trace_chunks.yml.example38
-rw-r--r--db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb54
-rw-r--r--db/schema_migrations/202109140953101
-rw-r--r--doc/administration/configure.md36
-rw-r--r--doc/administration/gitaly/faq.md2
-rw-r--r--doc/administration/gitaly/index.md111
-rw-r--r--doc/administration/gitaly/praefect.md4
-rw-r--r--doc/administration/nfs.md40
-rw-r--r--doc/administration/operations/moving_repositories.md2
-rw-r--r--doc/administration/packages/container_registry.md13
-rw-r--r--doc/administration/redis/replication_and_failover.md6
-rw-r--r--doc/administration/reference_architectures/10k_users.md2
-rw-r--r--doc/administration/reference_architectures/25k_users.md2
-rw-r--r--doc/administration/reference_architectures/2k_users.md2
-rw-r--r--doc/administration/reference_architectures/3k_users.md2
-rw-r--r--doc/administration/reference_architectures/50k_users.md2
-rw-r--r--doc/administration/reference_architectures/5k_users.md2
-rw-r--r--doc/administration/repository_storage_paths.md2
-rw-r--r--doc/api/api_resources.md3
-rw-r--r--doc/api/dependencies.md10
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/group_repository_storage_moves.md2
-rw-r--r--doc/api/integrations.md1473
-rw-r--r--doc/api/project_repository_storage_moves.md2
-rw-r--r--doc/api/services.md1472
-rw-r--r--doc/api/snippet_repository_storage_moves.md2
-rw-r--r--doc/api/usage_data.md2
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/development/fe_guide/storybook.md6
-rw-r--r--doc/development/redis.md2
-rw-r--r--doc/development/service_ping/dictionary.md2
-rw-r--r--doc/development/service_ping/metrics_dictionary.md2
-rw-r--r--doc/development/service_ping/review_guidelines.md2
-rw-r--r--doc/development/usage_ping/dictionary.md2
-rw-r--r--doc/user/profile/account/two_factor_authentication.md6
-rw-r--r--doc/user/project/integrations/asana.md6
-rw-r--r--doc/user/project/integrations/overview.md4
-rw-r--r--doc/user/project/integrations/pivotal_tracker.md2
-rw-r--r--doc/user/project/settings/import_export.md4
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/helpers/integrations_helpers.rb2
-rw-r--r--lib/api/import_bitbucket_server.rb4
-rw-r--r--lib/api/integrations.rb178
-rw-r--r--lib/api/invitations.rb2
-rw-r--r--lib/api/projects.rb5
-rw-r--r--lib/api/services.rb173
-rw-r--r--lib/api/users.rb8
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb21
-rw-r--r--lib/bulk_imports/ndjson_pipeline.rb6
-rw-r--r--lib/bulk_imports/projects/pipelines/issues_pipeline.rb15
-rw-r--r--lib/bulk_imports/projects/stage.rb6
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/auth/auth_finders.rb4
-rw-r--r--lib/gitlab/auth/request_authenticator.rb24
-rw-r--r--lib/gitlab/auth/two_factor_auth_verifier.rb4
-rw-r--r--lib/gitlab/fogbugz_import.rb11
-rw-r--r--lib/gitlab/fogbugz_import/client.rb2
-rw-r--r--lib/gitlab/fogbugz_import/http_adapter.rb21
-rw-r--r--lib/gitlab/health_checks/redis/rate_limiting_check.rb35
-rw-r--r--lib/gitlab/health_checks/redis/redis_check.rb3
-rw-r--r--lib/gitlab/import_export/group/import_export.yml1
-rw-r--r--lib/gitlab/import_export/project/import_export.yml2
-rw-r--r--lib/gitlab/instrumentation/redis.rb3
-rw-r--r--lib/gitlab/legacy_github_import/client.rb6
-rw-r--r--lib/gitlab/metrics/dashboard/service_selector.rb2
-rw-r--r--lib/gitlab/redis/cache.rb15
-rw-r--r--lib/gitlab/redis/queues.rb9
-rw-r--r--lib/gitlab/redis/rate_limiting.rb12
-rw-r--r--lib/gitlab/redis/shared_state.rb8
-rw-r--r--lib/gitlab/redis/wrapper.rb33
-rw-r--r--lib/gitlab/string_regex_marker.rb10
-rw-r--r--lib/tasks/frontend.rake17
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json2
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/chemlab-library-gitlab.gemspec4
-rw-r--r--qa/lib/gitlab/page/main/login.rb18
-rw-r--r--qa/lib/gitlab/page/main/login.stub.rb82
-rwxr-xr-xscripts/frontend/start_storybook.sh18
-rw-r--r--scripts/prepare_build.sh12
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb8
-rw-r--r--spec/controllers/admin/users_controller_spec.rb23
-rw-r--r--spec/controllers/import/gitea_controller_spec.rb42
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb63
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb46
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb10
-rw-r--r--spec/controllers/projects_controller_spec.rb41
-rw-r--r--spec/controllers/uploads_controller_spec.rb2
-rw-r--r--spec/features/callouts/security_newsletter_callout_spec.rb57
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/profiles/password_spec.rb78
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb88
-rw-r--r--spec/features/users/login_spec.rb1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/integration.json (renamed from spec/fixtures/api/schemas/public_api/v4/service.json)0
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/integrations.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/services.json4
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json17
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson2
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap99
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js98
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js37
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js21
-rw-r--r--spec/frontend/users_select/index_spec.js16
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js18
-rw-r--r--spec/frontend_integration/fixture_generators.yml5
-rw-r--r--spec/graphql/types/group_invitation_type_spec.rb2
-rw-r--r--spec/graphql/types/project_invitation_type_spec.rb2
-rw-r--r--spec/helpers/external_link_helper_spec.rb8
-rw-r--r--spec/helpers/icons_helper_spec.rb8
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb33
-rw-r--r--spec/initializers/100_patch_omniauth_oauth2_spec.rb56
-rw-r--r--spec/lib/banzai/filter/spaced_link_filter_spec.rb10
-rw-r--r--spec/lib/bulk_imports/ndjson_pipeline_spec.rb16
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb168
-rw-r--r--spec/lib/bulk_imports/projects/stage_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb70
-rw-r--r--spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb61
-rw-r--r--spec/lib/gitlab/auth_spec.rb59
-rw-r--r--spec/lib/gitlab/fogbugz_import/importer_spec.rb80
-rw-r--r--spec/lib/gitlab/git_access_spec.rb35
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb2
-rw-r--r--spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml10
-rw-r--r--spec/lib/gitlab/instrumentation/redis_spec.rb3
-rw-r--r--spec/lib/gitlab/legacy_github_import/client_spec.rb9
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb14
-rw-r--r--spec/lib/gitlab/redis/rate_limiting_spec.rb55
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb12
-rw-r--r--spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb47
-rw-r--r--spec/models/user_spec.rb82
-rw-r--r--spec/policies/global_policy_spec.rb18
-rw-r--r--spec/requests/api/import_bitbucket_server_spec.rb14
-rw-r--r--spec/requests/api/integrations_spec.rb363
-rw-r--r--spec/requests/api/invitations_spec.rb40
-rw-r--r--spec/requests/api/projects_spec.rb10
-rw-r--r--spec/requests/api/services_spec.rb361
-rw-r--r--spec/requests/api/users_spec.rb171
-rw-r--r--spec/requests/git_http_spec.rb16
-rw-r--r--spec/requests/lfs_http_spec.rb6
-rw-r--r--spec/requests/rack_attack_global_spec.rb45
-rw-r--r--spec/services/projects/destroy_service_spec.rb10
-rw-r--r--spec/support/redis.rb8
-rw-r--r--spec/support/redis/redis_helpers.rb5
-rw-r--r--spec/support/redis/redis_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb10
-rw-r--r--storybook/config/main.js3
-rw-r--r--storybook/config/preview.js7
-rw-r--r--storybook/config/webpack.config.js21
-rw-r--r--storybook/package.json4
-rw-r--r--storybook/yarn.lock17
-rw-r--r--workhorse/internal/artifacts/entry.go3
198 files changed, 4792 insertions, 2933 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 2799358be88..2eb52b59dea 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -100,6 +100,7 @@ overrides:
- 'scripts/**/*'
- '*.config.js'
- '*.config.*.js'
+ - storybook/config/*.js
rules:
'@gitlab/require-i18n-strings': off
import/no-extraneous-dependencies: off
diff --git a/.gitignore b/.gitignore
index f753a247563..5152ef20575 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,10 +40,7 @@ eslint-report.html
/config/initializers/smtp_settings.rb
/config/initializers/relative_url.rb
/config/resque.yml
-/config/redis.cache.yml
-/config/redis.queues.yml
-/config/redis.shared_state.yml
-/config/redis.trace_chunks.yml
+/config/redis.*.yml
/config/unicorn.rb
/config/puma.rb
/config/secrets.yml
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 48f85219ff4..586a6db8d5b 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -366,6 +366,11 @@ compile-storybook:
extends:
- .compile-storybook-base
- .frontend:rules:default-frontend-jobs
+ needs:
+ - job: "rspec frontend_fixture"
+ - job: "rspec-ee frontend_fixture"
+ optional: true
+ - job: "graphql-schema-dump"
artifacts:
name: storybook
expire_in: 31d
@@ -378,3 +383,6 @@ compile-storybook as-if-foss:
- .compile-storybook-base
- .as-if-foss
- .frontend:rules:default-frontend-jobs-as-if-foss
+ needs:
+ - job: "graphql-schema-dump"
+ - job: "rspec frontend_fixture as-if-foss"
diff --git a/.stylelintrc b/.stylelintrc
index a4331811eb3..488e34dd7d4 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -13,6 +13,7 @@
"./scripts/frontend/stylelint/stylelint-utility-classes.js",
],
"rules":{
+ "at-rule-disallowed-list": ["extend"],
"max-nesting-depth": [
3,
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0645b2a668b..8efd1a2b702 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,40 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 14.3.1 (2021-09-30)
+
+### Security (29 changes)
+
+- [Fix permissions check on project members import](gitlab-org/security/gitlab@63ba9ad2a1067eb74df493e273707bb64a13a197) ([merge request](gitlab-org/security/gitlab!1858))
+- [Require password param for 2FA changes](gitlab-org/security/gitlab@f246cfbd15344ba74a0182276bf63f0b5f1a4a31) ([merge request](gitlab-org/security/gitlab!1813))
+- [Respect disabled import sources when initiating import via API](gitlab-org/security/gitlab@046e964b0151fc8c58063281a39af063ffb678bd) ([merge request](gitlab-org/security/gitlab!1846))
+- [Return 404 if model id wasn't passed to UploadsController](gitlab-org/security/gitlab@747e6f0e4aec39462f296fd56b37df1c255d29cb) ([merge request](gitlab-org/security/gitlab!1843))
+- [Scrub artifacts signed URL in SendEntry logs](gitlab-org/security/gitlab@f6c57892ddc9518efaace1021346b42b4c805a1c) ([merge request](gitlab-org/security/gitlab!1840))
+- [Prevent double-impersonation and impersonation breakout](gitlab-org/security/gitlab@615d418f9315ca3b3619689c47201f618cf6bde9) ([merge request](gitlab-org/security/gitlab!1834))
+- [Clear session access tokens when starting/stopping impersonation](gitlab-org/security/gitlab@62c2e0d3ed73f2d7ded90d04fe232ff6ae2f6136) ([merge request](gitlab-org/security/gitlab!1831))
+- [Prevent users from bypassing 2FA on certain pages](gitlab-org/security/gitlab@0b41838b36da09a9230de4d8449040a701464de7) ([merge request](gitlab-org/security/gitlab!1827))
+- [Use validated URL when sending request to Gitea Importer](gitlab-org/security/gitlab@26731d762f6503fe1b8b509be11c56e77601a552) ([merge request](gitlab-org/security/gitlab!1822))
+- [Fix XSS in Jira link](gitlab-org/security/gitlab@d41060acb2aa151119042db9162a102d4e2c15ab) ([merge request](gitlab-org/security/gitlab!1819)) **GitLab Enterprise Edition**
+- [Fix fogbugz importer DNS Rebind SSRF](gitlab-org/security/gitlab@cc13d57c66cc65e6f920bdeab57b9fdb9d6baac1) ([merge request](gitlab-org/security/gitlab!1814))
+- [Remove related project access tokens when a project is deleted](gitlab-org/security/gitlab@d32c0d57d5b39601034c4c4ae983ea80c05db429) ([merge request](gitlab-org/security/gitlab!1810))
+- [Require group admin access to list pending invites](gitlab-org/security/gitlab@911bb0cb78e00934c491af59729fa84fffae7676) ([merge request](gitlab-org/security/gitlab!1793))
+- [Do not export and import repository_size_limit](gitlab-org/security/gitlab@0f3feca459895fc6665f8b0dfc16d4dcd7112944) ([merge request](gitlab-org/security/gitlab!1770))
+- [Escapes MR approval rule names correctly](gitlab-org/security/gitlab@4fcd97230bbc31780fe14d75694bb6433d57e677) ([merge request](gitlab-org/security/gitlab!1807))
+- [Filter shared groups autocomplete by permitted](gitlab-org/security/gitlab@b5144abb0516af61686402c2ad720967d11cb03c) ([merge request](gitlab-org/security/gitlab!1804)) **GitLab Enterprise Edition**
+- [Require access token for git when 2fa is required](gitlab-org/security/gitlab@ea22f67b47bf0d6c801f2bf6c9672c0ea5afd30c) ([merge request](gitlab-org/security/gitlab!1794))
+- [Prohibit anonymous access for specific user API endpoint](gitlab-org/security/gitlab@c52890997ad574812ae4da968f2f6ecfd9f7ff59) ([merge request](gitlab-org/security/gitlab!1792))
+- [Disable exporting pipeline triggers on project export](gitlab-org/security/gitlab@f7f18fbdd8e81a9b3e0650250316c7bb17ac1956) ([merge request](gitlab-org/security/gitlab!1791))
+- [Add pagination to dependencies API](gitlab-org/security/gitlab@203328889059564ba6085663b21355149c01e501) ([merge request](gitlab-org/security/gitlab!1726)) **GitLab Enterprise Edition**
+- [Do not allow status checks to exist with external protected branches](gitlab-org/security/gitlab@327d8080e7e7b0bc77b7933f8026ec0cf1abd99a) ([merge request](gitlab-org/security/gitlab!1788)) **GitLab Enterprise Edition**
+- [Permission check issuable template API data](gitlab-org/security/gitlab@de7851c2ab58c31df49c8a406ed0c3f3ad779e26) ([merge request](gitlab-org/security/gitlab!1785)) **GitLab Enterprise Edition**
+- [Apply account locking to password reset page](gitlab-org/security/gitlab@050dfa71191ffaea77a4a18e0dea1f3336f40db5) ([merge request](gitlab-org/security/gitlab!1782))
+- [Enforce configured scopes for Oauth applications](gitlab-org/security/gitlab@ce83bb14b5a4521f889086a439f1628041843589) ([merge request](gitlab-org/security/gitlab!1779))
+- [Verify state before using errors from OAuth2 OmniAuth providers](gitlab-org/security/gitlab@dcc2cad6c03255ac70f29ed9c0f6c8bc11ac1018) ([merge request](gitlab-org/security/gitlab!1776))
+- [Prevent moving epic issues to different group hierarchy](gitlab-org/security/gitlab@167601717f2ad46fee2320af6ac49674026501be) ([merge request](gitlab-org/security/gitlab!1772)) **GitLab Enterprise Edition**
+- [Fix GFM autocomplete xss](gitlab-org/security/gitlab@8816ab6af1d1aa752f22da7850d4d1c983f2d43a) ([merge request](gitlab-org/security/gitlab!1767))
+- [Prevent showing not allowed subgroup epics](gitlab-org/security/gitlab@b841c78c47b6a56b618186720bffc26922807356) ([merge request](gitlab-org/security/gitlab!1764)) **GitLab Enterprise Edition**
+- [Fix denial-of-service attack in Markdown parser](gitlab-org/security/gitlab@5e5973b5c28862381729408ba4df650c3d4f7ce0) ([merge request](gitlab-org/security/gitlab!1730))
+
## 14.3.0 (2021-09-21)
### Added (111 changes)
@@ -498,6 +532,39 @@ entry.
- [Remove the FF ci_reset_bridge_with_subsequent_jobs](gitlab-org/gitlab@a4a75095b9b0250d0b1bdadea90c8a4cd24449b2) ([merge request](gitlab-org/gitlab!68295))
- [Removes ci_same_stage_job_needs ff](gitlab-org/gitlab@5e509cf7aa90041a541b19dda563120a359f0bf9) ([merge request](gitlab-org/gitlab!68041))
+## 14.2.5 (2021-09-30)
+
+### Security (28 changes)
+
+- [Require password param for 2FA changes](gitlab-org/security/gitlab@5693760a3edf82774a4e19b9bb561be87316be54) ([merge request](gitlab-org/security/gitlab!1815))
+- [Fix permissions check on project members import](gitlab-org/security/gitlab@f9b4200427833e370638aa63851d6801a40c404c) ([merge request](gitlab-org/security/gitlab!1859))
+- [Respect disabled import sources when initiating import via API](gitlab-org/security/gitlab@3c9af055ece281fcaab0b8dcc277e0ce9133ad31) ([merge request](gitlab-org/security/gitlab!1847))
+- [Return 404 if model id wasn't passed to UploadsController](gitlab-org/security/gitlab@dd4616362040d2b812d69ff2ecf78e70ff4b9ae3) ([merge request](gitlab-org/security/gitlab!1844))
+- [Scrub artifacts signed URL in SendEntry logs](gitlab-org/security/gitlab@41d275bb73943ba6f970d6678b49c9336194af35) ([merge request](gitlab-org/security/gitlab!1841))
+- [Prevent double-impersonation and impersonation breakout](gitlab-org/security/gitlab@c0ab498adda057f4d87969d74c32a3ea95df297c) ([merge request](gitlab-org/security/gitlab!1835))
+- [Clear session access tokens when starting/stopping impersonation](gitlab-org/security/gitlab@fd39d88b348d525818820d2496afe08612420023) ([merge request](gitlab-org/security/gitlab!1832))
+- [Use validated URL when sending request to Gitea Importer](gitlab-org/security/gitlab@328e3c726c693b32666e0fb32eda0b7a6f22d8ad) ([merge request](gitlab-org/security/gitlab!1821))
+- [Fix XSS in Jira link](gitlab-org/security/gitlab@868d8b9c4a1e9e9019a7ff51da11f75051e452c3) ([merge request](gitlab-org/security/gitlab!1817)) **GitLab Enterprise Edition**
+- [Fix fogbugz importer DNS Rebind SSRF](gitlab-org/security/gitlab@4f4b5a15a3508084f921442b3a7f42ba0448f1bb) ([merge request](gitlab-org/security/gitlab!1681))
+- [Remove related project access tokens when a project is deleted](gitlab-org/security/gitlab@282e81198f80f1fda912da5bc6f671d778b19ca9) ([merge request](gitlab-org/security/gitlab!1811))
+- [Require group admin access to list pending invites](gitlab-org/security/gitlab@1ce85345787025222c915fe5fa314bad8994b6ba) ([merge request](gitlab-org/security/gitlab!1720))
+- [Do not export and import repository_size_limit](gitlab-org/security/gitlab@359f14e41dfc355a13041cdf1dbcd082c254200c) ([merge request](gitlab-org/security/gitlab!1769))
+- [Escapes MR approval rule names correctly](gitlab-org/security/gitlab@d84739982599197ff337d69d818634544270e142) ([merge request](gitlab-org/security/gitlab!1808))
+- [Filter shared groups autocomplete by permitted](gitlab-org/security/gitlab@3a2b4c7ff1eb2ba3e84840ba2800c13d6491d726) ([merge request](gitlab-org/security/gitlab!1805)) **GitLab Enterprise Edition**
+- [Require access token for git when 2fa is required](gitlab-org/security/gitlab@deb4e7e5f941c82450d382c1b85f6325e367394f) ([merge request](gitlab-org/security/gitlab!1795))
+- [Disable exporting pipeline triggers on project export](gitlab-org/security/gitlab@417761bb2f67f03bfe803163bad97da7b9fa088b) ([merge request](gitlab-org/security/gitlab!1789))
+- [Add pagination to dependencies API](gitlab-org/security/gitlab@2f84755ba54580df126054a561d8cc4731f936d3) ([merge request](gitlab-org/security/gitlab!1724)) **GitLab Enterprise Edition**
+- [Permission check issuable template API data](gitlab-org/security/gitlab@a90614e2efc813ca5f13a9aa9b51f13f0e8934aa) ([merge request](gitlab-org/security/gitlab!1786)) **GitLab Enterprise Edition**
+- [Apply account locking to password reset page](gitlab-org/security/gitlab@6bbd77c0748e59eacff51edb6264d6099ee14a38) ([merge request](gitlab-org/security/gitlab!1783))
+- [Enforce configured scopes for Oauth applications](gitlab-org/security/gitlab@a9f44bb19cbfc460cd05627a80ef17c39cdde86b) ([merge request](gitlab-org/security/gitlab!1780))
+- [Verify state before using errors from OAuth2 OmniAuth providers](gitlab-org/security/gitlab@6f70292d0fa3efbe99c44748a463df189830cc35) ([merge request](gitlab-org/security/gitlab!1777))
+- [Prevent moving epic issues to different group hierarchy](gitlab-org/security/gitlab@979d40003794014d5930709a257e9a5c75df10e6) ([merge request](gitlab-org/security/gitlab!1773)) **GitLab Enterprise Edition**
+- [Prevent showing not allowed subgroup epics](gitlab-org/security/gitlab@2f72e4062f6cd7256ffff31172b00c012a5910e1) ([merge request](gitlab-org/security/gitlab!1765)) **GitLab Enterprise Edition**
+- [Do not allow status checks to exist with external protected branches](gitlab-org/security/gitlab@dd08837d054c574f94f80e806cc7b49de342cc57) ([merge request](gitlab-org/security/gitlab!1762)) **GitLab Enterprise Edition**
+- [Fix GFM autocomplete xss](gitlab-org/security/gitlab@5afba618ef89fdce544f498a30e7366e3f6cb788) ([merge request](gitlab-org/security/gitlab!1747))
+- [Prohibit anonymous access for specific user API endpoint](gitlab-org/security/gitlab@a813bd8a8f07ffa0477efd3a3936b436e5ec6b17) ([merge request](gitlab-org/security/gitlab!1736))
+- [Fix denial-of-service attack in Markdown parser](gitlab-org/security/gitlab@f618ad9c104882ac5f707b162e8119805252019e) ([merge request](gitlab-org/security/gitlab!1729))
+
## 14.2.4 (2021-09-17)
### Fixed (2 changes)
@@ -1089,6 +1156,39 @@ entry.
- [Add helpful text to URL group validation and limit text](gitlab-org/gitlab@59a5a6266cb0d5434596170ffa36e4e74b8d2c2c) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65369)) **GitLab Enterprise Edition**
- [Refactor external storage admin area configuration UI and docs](gitlab-org/gitlab@497ba4fc8f4ec1d234c9f5f1ec5c69712b8c7cb3) ([merge request](gitlab-org/gitlab!66219))
+## 14.1.7 (2021-09-30)
+
+### Security (28 changes)
+
+- [Require password param for 2FA changes](gitlab-org/security/gitlab@4e16401a77264ef3127f0bb314fa0abab11216c7) ([merge request](gitlab-org/security/gitlab!1816))
+- [Fix permissions check on project members import](gitlab-org/security/gitlab@be54b3f4890fa89d58cb02be79c65025f606bd6c) ([merge request](gitlab-org/security/gitlab!1860))
+- [Respect disabled import sources when initiating import via API](gitlab-org/security/gitlab@b76b6229c93447954efc5719e6dd61eb601afdc4) ([merge request](gitlab-org/security/gitlab!1848))
+- [Return 404 if model id wasn't passed to UploadsController](gitlab-org/security/gitlab@8ab1cfaafba21a9e90ef41677993af2afdcd920a) ([merge request](gitlab-org/security/gitlab!1845))
+- [Scrub artifacts signed URL in SendEntry logs](gitlab-org/security/gitlab@5bae93b2f085d5dac58e411bffb5ca518fe7df98) ([merge request](gitlab-org/security/gitlab!1842))
+- [Prevent double-impersonation and impersonation breakout](gitlab-org/security/gitlab@7c82d0f2a1dbfb0a23d6a5cdaee448307ffc6972) ([merge request](gitlab-org/security/gitlab!1836))
+- [Clear session access tokens when starting/stopping impersonation](gitlab-org/security/gitlab@a4d529eb7c8eeb3e9c42ae3d81514d79a905cdb7) ([merge request](gitlab-org/security/gitlab!1833))
+- [Use validated URL when sending request to Gitea Importer](gitlab-org/security/gitlab@b30536e6c9aa969c76bcd167f00db5a0e07ace7a) ([merge request](gitlab-org/security/gitlab!1820))
+- [Fix XSS in Jira link](gitlab-org/security/gitlab@9ace10c46744ee220c649d2da0eeb3e99216ee7d) ([merge request](gitlab-org/security/gitlab!1625)) **GitLab Enterprise Edition**
+- [Fix fogbugz importer DNS Rebind SSRF](gitlab-org/security/gitlab@9d7107665d6ed931ef4b2feeb0287bc71b89232c) ([merge request](gitlab-org/security/gitlab!1682))
+- [Remove related project access tokens when a project is deleted](gitlab-org/security/gitlab@b86096865949f07f6a2020603959117d9c84877b) ([merge request](gitlab-org/security/gitlab!1812))
+- [Require group admin access to list pending invites](gitlab-org/security/gitlab@404b344edd61b2f13c3498cd545c2b40165ee536) ([merge request](gitlab-org/security/gitlab!1721))
+- [Do not export and import repository_size_limit](gitlab-org/security/gitlab@56f563980f944f1a5e3935ad82070e6719cd5a0c) ([merge request](gitlab-org/security/gitlab!1768))
+- [Escapes MR approval rule names correctly](gitlab-org/security/gitlab@ea64f981ce70a0e1e6ee58e64a6007e82f48e071) ([merge request](gitlab-org/security/gitlab!1809))
+- [Filter shared groups autocomplete by permitted](gitlab-org/security/gitlab@59999ab27cba402589b27d204cf29678100e948b) ([merge request](gitlab-org/security/gitlab!1806)) **GitLab Enterprise Edition**
+- [Require access token for git when 2fa is required](gitlab-org/security/gitlab@6a4a75efd7685a69ffa7cc4c027c7058013cca45) ([merge request](gitlab-org/security/gitlab!1796))
+- [Disable exporting pipeline triggers on project export](gitlab-org/security/gitlab@8a8c78ed054def210013a849195939d7888fcf65) ([merge request](gitlab-org/security/gitlab!1790))
+- [Add pagination to dependencies API](gitlab-org/security/gitlab@2a963ad670c60d1f3078fdf446ea755c5862fa26) ([merge request](gitlab-org/security/gitlab!1725)) **GitLab Enterprise Edition**
+- [Permission check issuable template API data](gitlab-org/security/gitlab@9d95d13bc714e46b5e3697288c4b398cb5aee88b) ([merge request](gitlab-org/security/gitlab!1787)) **GitLab Enterprise Edition**
+- [Apply account locking to password reset page](gitlab-org/security/gitlab@47ee79b1983de886f5ebe04b2975c2e37aa938ce) ([merge request](gitlab-org/security/gitlab!1784))
+- [Enforce configured scopes for Oauth applications](gitlab-org/security/gitlab@acf2d894c91aa7fb72ea32b10e50e94441885399) ([merge request](gitlab-org/security/gitlab!1781))
+- [Verify state before using errors from OAuth2 OmniAuth providers](gitlab-org/security/gitlab@20073576508aa239e52d8ff911c1dfd3df8af670) ([merge request](gitlab-org/security/gitlab!1778))
+- [Prevent moving epic issues to different group hierarchy](gitlab-org/security/gitlab@93c6ec69b7bc6c9124a2a5350cebebb57f63a28f) ([merge request](gitlab-org/security/gitlab!1774)) **GitLab Enterprise Edition**
+- [Prevent showing not allowed subgroup epics](gitlab-org/security/gitlab@72a11e72425a033f3464d6ff12b4d06e12ec9faf) ([merge request](gitlab-org/security/gitlab!1766)) **GitLab Enterprise Edition**
+- [Do not allow status checks to exist with external protected branches](gitlab-org/security/gitlab@8f96c013ccbbe9c52b3f03fb0d247debb1b157a8) ([merge request](gitlab-org/security/gitlab!1763)) **GitLab Enterprise Edition**
+- [Fix GFM autocomplete xss](gitlab-org/security/gitlab@fd92dabddff5ae5d67a98aef5d858438520a2f06) ([merge request](gitlab-org/security/gitlab!1748))
+- [Prohibit anonymous access for specific user API endpoint](gitlab-org/security/gitlab@2e8a386430309a931dbbd47fba7540a53399ad64) ([merge request](gitlab-org/security/gitlab!1737))
+- [Fix denial-of-service attack in Markdown parser](gitlab-org/security/gitlab@5b6ed5212f880e2397dbea9ffc74cf0a35bd4411) ([merge request](gitlab-org/security/gitlab!1728))
+
## 14.1.6 (2021-09-27)
### Fixed (1 change)
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
new file mode 100644
index 00000000000..280c222c380
--- /dev/null
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlFormInput, GlFormGroup, GlButton, GlForm } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { __ } from '~/locale';
+
+export const i18n = {
+ currentPassword: __('Current password'),
+ confirmWebAuthn: __(
+ 'Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.',
+ ),
+ confirm: __('Are you sure? This will invalidate your registered applications and U2F devices.'),
+ disableTwoFactor: __('Disable two-factor authentication'),
+ regenerateRecoveryCodes: __('Regenerate recovery codes'),
+};
+
+export default {
+ name: 'ManageTwoFactorForm',
+ i18n,
+ components: {
+ GlForm,
+ GlFormInput,
+ GlFormGroup,
+ GlButton,
+ },
+ inject: [
+ 'webauthnEnabled',
+ 'profileTwoFactorAuthPath',
+ 'profileTwoFactorAuthMethod',
+ 'codesProfileTwoFactorAuthPath',
+ 'codesProfileTwoFactorAuthMethod',
+ ],
+ data() {
+ return {
+ method: '',
+ action: '#',
+ };
+ },
+ computed: {
+ confirmText() {
+ if (this.webauthnEnabled) {
+ return i18n.confirmWebAuthn;
+ }
+
+ return i18n.confirm;
+ },
+ },
+ methods: {
+ handleFormSubmit(event) {
+ this.method = event.submitter.dataset.formMethod;
+ this.action = event.submitter.dataset.formAction;
+ },
+ },
+ csrf,
+};
+</script>
+
+<template>
+ <gl-form
+ class="gl-display-inline-block"
+ method="post"
+ :action="action"
+ @submit="handleFormSubmit($event)"
+ >
+ <input type="hidden" name="_method" data-testid="test-2fa-method-field" :value="method" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+
+ <gl-form-group :label="$options.i18n.currentPassword" label-for="current-password">
+ <gl-form-input
+ id="current-password"
+ type="password"
+ name="current_password"
+ required
+ data-qa-selector="current_password_field"
+ />
+ </gl-form-group>
+
+ <gl-button
+ type="submit"
+ class="btn-danger gl-mr-3 gl-display-inline-block"
+ data-testid="test-2fa-disable-button"
+ variant="danger"
+ :data-confirm="confirmText"
+ :data-form-action="profileTwoFactorAuthPath"
+ :data-form-method="profileTwoFactorAuthMethod"
+ >
+ {{ $options.i18n.disableTwoFactor }}
+ </gl-button>
+ <gl-button
+ type="submit"
+ class="gl-display-inline-block"
+ data-testid="test-2fa-regenerate-codes-button"
+ :data-form-action="codesProfileTwoFactorAuthPath"
+ :data-form-method="codesProfileTwoFactorAuthMethod"
+ >
+ {{ $options.i18n.regenerateRecoveryCodes }}
+ </gl-button>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/authentication/two_factor_auth/index.js b/app/assets/javascripts/authentication/two_factor_auth/index.js
index 5e59c44e8cd..f663c0705e6 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/index.js
+++ b/app/assets/javascripts/authentication/two_factor_auth/index.js
@@ -1,8 +1,39 @@
import Vue from 'vue';
import { updateHistory, removeParams } from '~/lib/utils/url_utility';
+import ManageTwoFactorForm from './components/manage_two_factor_form.vue';
import RecoveryCodes from './components/recovery_codes.vue';
import { SUCCESS_QUERY_PARAM } from './constants';
+export const initManageTwoFactorForm = () => {
+ const el = document.querySelector('.js-manage-two-factor-form');
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ webauthnEnabled = false,
+ profileTwoFactorAuthPath = '',
+ profileTwoFactorAuthMethod = '',
+ codesProfileTwoFactorAuthPath = '',
+ codesProfileTwoFactorAuthMethod = '',
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ webauthnEnabled,
+ profileTwoFactorAuthPath,
+ profileTwoFactorAuthMethod,
+ codesProfileTwoFactorAuthPath,
+ codesProfileTwoFactorAuthMethod,
+ },
+ render(createElement) {
+ return createElement(ManageTwoFactorForm);
+ },
+ });
+};
+
export const initRecoveryCodes = () => {
const el = document.querySelector('.js-2fa-recovery-codes');
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 470c785f7e4..cb63c86a4fa 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
-import { escape, sortBy, template } from 'lodash';
+import { escape as lodashEscape, sortBy, template } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
@@ -11,8 +11,21 @@ import { spriteIcon } from './lib/utils/common_utils';
import { parsePikadayDate } from './lib/utils/datetime_utility';
import glRegexp from './lib/utils/regexp';
-function sanitize(str) {
- return str.replace(/<(?:.|\n)*?>/gm, '');
+/**
+ * Escapes user input before we pass it to at.js, which
+ * renders it as HTML in the autocomplete dropdown.
+ *
+ * at.js allows you to reference data using `${}` syntax
+ * (e.g. ${search}) which it replaces with the actual data
+ * before rendering it in the autocomplete dropdown.
+ * To prevent user input from executing this `${}` syntax,
+ * we also need to escape the $ character.
+ *
+ * @param string user input
+ * @return {string} escaped user input
+ */
+function escape(string) {
+ return lodashEscape(string).replace(/\$/g, '&dollar;');
}
function createMemberSearchString(member) {
@@ -44,8 +57,8 @@ export function membersBeforeSave(members) {
return {
username: member.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
- title: sanitize(title),
- search: sanitize(createMemberSearchString(member)),
+ title,
+ search: createMemberSearchString(member),
icon: avatarIcon,
availability: member?.availability,
};
@@ -366,7 +379,7 @@ class GfmAutoComplete {
}
return {
id: i.iid,
- title: sanitize(i.title),
+ title: i.title,
reference: i.reference,
search: `${i.iid} ${i.title}`,
};
@@ -404,7 +417,7 @@ class GfmAutoComplete {
return {
id: m.iid,
- title: sanitize(m.title),
+ title: m.title,
search: m.title,
expired,
dueDate,
@@ -456,7 +469,7 @@ class GfmAutoComplete {
}
return {
id: m.iid,
- title: sanitize(m.title),
+ title: m.title,
reference: m.reference,
search: `${m.iid} ${m.title}`,
};
@@ -492,7 +505,7 @@ class GfmAutoComplete {
beforeSave(merges) {
if (GfmAutoComplete.isLoading(merges)) return merges;
return $.map(merges, (m) => ({
- title: sanitize(m.title),
+ title: m.title,
color: m.color,
search: m.title,
set: m.set,
@@ -586,7 +599,7 @@ class GfmAutoComplete {
}
return {
id: m.id,
- title: sanitize(m.title),
+ title: m.title,
search: `${m.id} ${m.title}`,
};
});
diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js
index 25b60dcd14a..f212bf80bd7 100644
--- a/app/assets/javascripts/lib/utils/regexp.js
+++ b/app/assets/javascripts/lib/utils/regexp.js
@@ -1,6 +1,5 @@
/**
* Regexp utility for the convenience of working with regular expressions.
- *
*/
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
@@ -8,4 +7,9 @@
const unicodeLetters =
'\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
-export default { unicodeLetters };
+/**
+ * A regex that matches all single quotes in a string
+ */
+export const allSingleQuotes = /'/g;
+
+export default { unicodeLetters, allSingleQuotes };
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 5ee00464a8b..419afa0a0a9 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -4,6 +4,7 @@ import {
TRUNCATE_WIDTH_DEFAULT_WIDTH,
TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
} from '~/lib/utils/constants';
+import { allSingleQuotes } from '~/lib/utils/regexp';
/**
* Adds a , to a string composed by numbers, at every 3 chars.
@@ -479,3 +480,17 @@ export const markdownConfig = {
ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
ALLOW_DATA_ATTR: false,
};
+
+/**
+ * Escapes a string into a shell string, for example
+ * when you want to give a user the command to checkout
+ * a branch.
+ *
+ * It replaces all single-quotes with an escaped "'\''"
+ * that is interpreted by shell as a single-quote. It also
+ * encapsulates the string in single-quotes.
+ *
+ * If the branch is `fix-'bug-behavior'`, that should be
+ * escaped to `'fix-'\''bug-behavior'\'''`.
+ */
+export const escapeShellString = (str) => `'${str.replace(allSingleQuotes, () => "'\\''")}'`;
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 50835333a54..f6f136f2402 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -1,5 +1,5 @@
import { mount2faRegistration } from '~/authentication/mount_2fa';
-import { initRecoveryCodes } from '~/authentication/two_factor_auth';
+import { initRecoveryCodes, initManageTwoFactorForm } from '~/authentication/two_factor_auth';
import { parseBoolean } from '~/lib/utils/common_utils';
const twoFactorNode = document.querySelector('.js-two-factor-auth');
@@ -14,3 +14,5 @@ if (skippable) {
mount2faRegistration();
initRecoveryCodes();
+
+initManageTwoFactorForm();
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 8170a1f8443..a7f8704b559 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -9,6 +9,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-registration-enabled-callout',
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
+ '.js-security-newsletter-callout',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 69b3c27173f..8ed92e6b948 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -842,7 +842,7 @@ UsersSelect.prototype.renderApprovalRules = function (elsClassName, approvalRule
const [rule] = approvalRules;
const countText = sprintf(__('(+%{count}&nbsp;rules)'), { count });
const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : '';
- const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
+ const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : escape(rule.name);
return `<div class="gl-display-flex gl-font-sm">
<span class="gl-text-truncate" title="${ruleName}">${ruleName}</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
index 7532eabee8a..68cff1368af 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -1,6 +1,7 @@
<script>
/* eslint-disable @gitlab/require-i18n-strings */
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
+import { escapeShellString } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -75,20 +76,31 @@ export default {
},
computed: {
mergeInfo1() {
+ const escapedOriginBranch = escapeShellString(`origin/${this.sourceBranch}`);
+
return this.isFork
- ? `git fetch "${this.sourceProjectDefaultUrl}" ${this.sourceBranch}\ngit checkout -b "${this.sourceProjectPath}-${this.sourceBranch}" FETCH_HEAD`
- : `git fetch origin\ngit checkout -b "${this.sourceBranch}" "origin/${this.sourceBranch}"`;
+ ? `git fetch "${this.sourceProjectDefaultUrl}" ${this.escapedSourceBranch}\ngit checkout -b ${this.escapedForkBranch} FETCH_HEAD`
+ : `git fetch origin\ngit checkout -b ${this.escapedSourceBranch} ${escapedOriginBranch}`;
},
mergeInfo2() {
return this.isFork
- ? `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff "${this.sourceProjectPath}-${this.sourceBranch}"`
- : `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff "${this.sourceBranch}"`;
+ ? `git fetch origin\ngit checkout ${this.escapedTargetBranch}\ngit merge --no-ff ${this.escapedForkBranch}`
+ : `git fetch origin\ngit checkout ${this.escapedTargetBranch}\ngit merge --no-ff ${this.escapedSourceBranch}`;
},
mergeInfo3() {
return this.canMerge
- ? `git push origin "${this.targetBranch}"`
+ ? `git push origin ${this.escapedTargetBranch}`
: __('Note that pushing to GitLab requires write access to this repository.');
},
+ escapedForkBranch() {
+ return escapeShellString(`${this.sourceProjectPath}-${this.sourceBranch}`);
+ },
+ escapedTargetBranch() {
+ return escapeShellString(this.targetBranch);
+ },
+ escapedSourceBranch() {
+ return escapeShellString(this.sourceBranch);
+ },
},
};
</script>
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index ba24e3e619b..086db1004a6 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -15,7 +15,13 @@ class Admin::DashboardController < Admin::ApplicationController
@groups = Group.order_id_desc.with_route.limit(10)
@notices = Gitlab::ConfigChecker::PumaRuggedChecker.check
@notices += Gitlab::ConfigChecker::ExternalDatabaseChecker.check
- @redis_versions = [Gitlab::Redis::Queues, Gitlab::Redis::SharedState, Gitlab::Redis::Cache, Gitlab::Redis::TraceChunks].map(&:version).uniq
+ @redis_versions = [
+ Gitlab::Redis::Queues,
+ Gitlab::Redis::SharedState,
+ Gitlab::Redis::Cache,
+ Gitlab::Redis::TraceChunks,
+ Gitlab::Redis::RateLimiting
+ ].map(&:version).uniq
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 9c556d16913..cdfb3a32f4c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -45,10 +45,11 @@ class Admin::UsersController < Admin::ApplicationController
end
def impersonate
- if can?(user, :log_in)
+ if can?(user, :log_in) && !impersonation_in_progress?
session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user)
+ clear_access_token_session_keys!
log_impersonation_event
@@ -57,7 +58,9 @@ class Admin::UsersController < Admin::ApplicationController
redirect_to root_path
else
flash[:alert] =
- if user.blocked?
+ if impersonation_in_progress?
+ _("You are already impersonating another user")
+ elsif user.blocked?
_("You cannot impersonate a blocked user")
elsif user.internal?
_("You cannot impersonate an internal user")
diff --git a/app/controllers/concerns/impersonation.rb b/app/controllers/concerns/impersonation.rb
index a4f2c263eb4..539dd9ad69d 100644
--- a/app/controllers/concerns/impersonation.rb
+++ b/app/controllers/concerns/impersonation.rb
@@ -3,6 +3,12 @@
module Impersonation
include Gitlab::Utils::StrongMemoize
+ SESSION_KEYS_TO_DELETE = %w(
+ github_access_token gitea_access_token gitlab_access_token
+ bitbucket_token bitbucket_refresh_token bitbucket_server_personal_access_token
+ bulk_import_gitlab_access_token fogbugz_token
+ ).freeze
+
def current_user
user = super
@@ -14,7 +20,7 @@ module Impersonation
protected
def check_impersonation_availability
- return unless session[:impersonator_id]
+ return unless impersonation_in_progress?
unless Gitlab.config.gitlab.impersonation_enabled
stop_impersonation
@@ -27,14 +33,25 @@ module Impersonation
warden.set_user(impersonator, scope: :user)
session[:impersonator_id] = nil
+ clear_access_token_session_keys!
current_user
end
+ def impersonation_in_progress?
+ session[:impersonator_id].present?
+ end
+
def log_impersonation_event
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{current_user.username}")
end
+ def clear_access_token_session_keys!
+ access_tokens_keys = session.keys & SESSION_KEYS_TO_DELETE
+
+ access_tokens_keys.each { |key| session.delete(key) }
+ end
+
def impersonator
strong_memoize(:impersonator) do
User.find(session[:impersonator_id]) if session[:impersonator_id]
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 99b0b775217..20407a75534 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -16,6 +16,7 @@ class HealthController < ActionController::Base
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
Gitlab::HealthChecks::Redis::TraceChunksCheck,
+ Gitlab::HealthChecks::Redis::RateLimitingCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 5a4eef352b8..32c9da67e90 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -66,11 +66,13 @@ class Import::GiteaController < Import::GithubController
override :client_options
def client_options
- { host: provider_url, api_version: 'v1' }
+ verified_url, provider_hostname = verify_blocked_uri
+
+ { host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s, api_version: 'v1', hostname: provider_hostname }
end
def verify_blocked_uri
- Gitlab::UrlBlocker.validate!(
+ @verified_url_and_hostname ||= Gitlab::UrlBlocker.validate!(
provider_url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index 85e901eb3eb..c8c2dd1c7d6 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -47,6 +47,8 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_attributes[:password_automatically_set] = false
unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
+ handle_invalid_current_password_attempt!
+
redirect_to edit_profile_password_path, alert: _('You must provide a valid current password')
return
end
@@ -85,6 +87,12 @@ class Profiles::PasswordsController < Profiles::ApplicationController
render_404 unless @user.allow_password_authentication?
end
+ def handle_invalid_current_password_attempt!
+ Gitlab::AppLogger.info(message: 'Invalid current password when attempting to update user password', username: @user.username, ip: request.remote_ip)
+
+ @user.increment_failed_attempts!
+ end
+
def user_params
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 5eb46421583..d1b9485f06d 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -3,6 +3,8 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement
before_action :ensure_verified_primary_email, only: [:show, :create]
+ before_action :validate_current_password, only: [:create, :codes, :destroy]
+
before_action do
push_frontend_feature_flag(:webauthn)
end
@@ -134,6 +136,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
+ def validate_current_password
+ return if current_user.valid_password?(params[:current_password])
+
+ current_user.increment_failed_attempts!
+
+ redirect_to profile_two_factor_auth_path, alert: _('You must provide a valid current password')
+ end
+
def build_qr_code
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d0987492d2d..b979276437c 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -34,13 +34,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def import
- @projects = current_user.authorized_projects.order_id_desc
+ @projects = Project.visible_to_user_and_access_level(current_user, Gitlab::Access::MAINTAINER).order_id_desc
end
def apply_import
source_project = Project.find(params[:source_project_id])
- if can?(current_user, :read_project_member, source_project)
+ if can?(current_user, :admin_project_member, source_project)
status = @project.team.import(source_project, current_user)
notice = status ? "Successfully imported" : "Import failed"
else
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index dba85ab2440..7ebd93d4a5d 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -19,6 +19,7 @@ class ProjectsController < Projects::ApplicationController
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create, :resolve]
before_action :repository, except: [:index, :new, :create, :resolve]
+ before_action :verify_git_import_enabled, only: [:create]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
before_action :authorize_download_code!, only: [:refs]
@@ -496,6 +497,10 @@ class ProjectsController < Projects::ApplicationController
url_for(safe_params)
end
+ def verify_git_import_enabled
+ render_404 if project_params[:import_url] && !git_import_enabled?
+ end
+
def project_export_enabled
render_404 unless Gitlab::CurrentSettings.project_export_enabled?
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 4077a3d3dac..d040ac7f76c 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -36,14 +36,10 @@ class UploadsController < ApplicationController
end
def find_model
- return unless params[:id]
-
upload_model_class.find(params[:id])
end
def authorize_access!
- return unless model
-
authorized =
case model
when Note
@@ -68,8 +64,6 @@ class UploadsController < ApplicationController
end
def authorize_create_access!
- return unless model
-
authorized =
case model
when User
diff --git a/app/graphql/types/group_invitation_type.rb b/app/graphql/types/group_invitation_type.rb
index 06a997bbc14..9410253553c 100644
--- a/app/graphql/types/group_invitation_type.rb
+++ b/app/graphql/types/group_invitation_type.rb
@@ -3,7 +3,7 @@
module Types
class GroupInvitationType < BaseObject
expose_permissions Types::PermissionTypes::Group
- authorize :read_group
+ authorize :admin_group
implements InvitationInterface
diff --git a/app/graphql/types/project_invitation_type.rb b/app/graphql/types/project_invitation_type.rb
index 507dc2d59c3..b76f05e289f 100644
--- a/app/graphql/types/project_invitation_type.rb
+++ b/app/graphql/types/project_invitation_type.rb
@@ -9,7 +9,7 @@ module Types
implements InvitationInterface
- authorize :read_project
+ authorize :admin_project
field :project, Types::ProjectType, null: true,
description: 'Project ID for the project of the invitation.'
diff --git a/app/helpers/external_link_helper.rb b/app/helpers/external_link_helper.rb
index 058302d1ed8..c951d0daf96 100644
--- a/app/helpers/external_link_helper.rb
+++ b/app/helpers/external_link_helper.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
module ExternalLinkHelper
+ include ActionView::Helpers::TextHelper
+
def external_link(body, url, options = {})
- link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
+ link = link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
"#{body}#{sprite_icon('external-link', css_class: 'gl-ml-1')}".html_safe
end
+ sanitize(link, tags: %w(a svg use), attributes: %w(target rel data-testid class href).concat(options.stringify_keys.keys))
end
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 096a3f2269b..c38b4a7aedf 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -44,7 +44,7 @@ module IconsHelper
content_tag(
:svg,
- content_tag(:use, '', { 'xlink:href' => "#{sprite_icon_path}##{icon_name}" } ),
+ content_tag(:use, '', { 'href' => "#{sprite_icon_path}##{icon_name}" } ),
class: css_classes.empty? ? nil : css_classes.join(' '),
data: { testid: "#{icon_name}-icon" }
)
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 2c3dc243d85..1c67ca983fa 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -10,6 +10,7 @@ module UserCalloutsHelper
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
INVITE_MEMBERS_BANNER = 'invite_members_banner'
+ SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@@ -64,6 +65,11 @@ module UserCalloutsHelper
!multiple_members?(group)
end
+ def show_security_newsletter_user_callout?
+ current_user&.admin? &&
+ !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 6519cc2699d..99cec647a98 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -31,7 +31,6 @@ class ProjectStatistics < ApplicationRecord
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
- scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
def total_repository_size
repository_size + lfs_objects_size
diff --git a/app/models/user.rb b/app/models/user.rb
index 6bc480b7a1d..832f8c879af 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1886,7 +1886,8 @@ class User < ApplicationRecord
def password_expired_if_applicable?
return false if bot?
- return false unless password_expired? && password_automatically_set?
+ return false unless password_expired?
+ return false if password_automatically_set?
return false unless allow_password_authentication?
true
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 04bc29755f8..b990aedd4f8 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -36,7 +36,8 @@ class UserCallout < ApplicationRecord
trial_status_reminder_d3: 35, # EE-only
security_configuration_devops_alert: 36, # EE-only
profile_personal_access_token_expiry: 37, # EE-only
- terraform_notification_dismissed: 38
+ terraform_notification_dismissed: 38,
+ security_newsletter_callout: 39
}
validates :feature_name,
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index e27ea7c07e5..afa8de04fca 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -117,6 +117,7 @@ module Projects
trash_relation_repositories!
trash_project_repositories!
destroy_web_hooks!
+ destroy_project_bots!
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
@@ -149,6 +150,16 @@ module Projects
end
end
+ # The project can have multiple project bots with personal access tokens generated.
+ # We need to remove them when a project is deleted
+ # rubocop: disable CodeReuse/ActiveRecord
+ def destroy_project_bots!
+ project.members.includes(:user).references(:user).merge(User.project_bot).each do |member|
+ Users::DestroyService.new(current_user).execute(member.user, skip_authorization: true)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def remove_registry_tags
return true unless Gitlab.config.registry.enabled
return false unless remove_legacy_registry_tags
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
new file mode 100644
index 00000000000..ece0f7ca4d9
--- /dev/null
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -0,0 +1,14 @@
+- return unless show_security_newsletter_user_callout?
+
+= render 'shared/global_alert',
+ title: s_('AdminArea|Get security updates from GitLab and stay up to date'),
+ variant: :tip,
+ alert_class: 'js-security-newsletter-callout',
+ is_contained: true,
+ alert_data: { feature_id: UserCalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ close_button_data: { testid: 'close-security-newsletter-callout' } do
+ .gl-alert-body
+ = s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
+ .gl-alert-actions
+ = link_to 'https://about.gitlab.com/company/preference-center/', target: '_blank', rel: 'noreferrer noopener', class: 'deferred-link gl-alert-action btn-confirm btn-md gl-button' do
+ = s_('AdminArea|Sign up for the GitLab newsletter')
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 97b3a757a3f..681e7ccb613 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -4,6 +4,7 @@
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
= render_if_exists 'shared/qrtly_reconciliation_alert'
+= render 'admin/dashboard/security_newsletter_callout'
- if @notices
- @notices.each do |notice|
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 927b6d4edef..d1d6b6301b8 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -17,13 +17,7 @@
= _("You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.")
%p
= _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.')
- %div
- = link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
- method: :delete,
- data: { confirm: webauthn_enabled ? _('Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.') : _('Are you sure? This will invalidate your registered applications and U2F devices.') },
- class: 'gl-button btn btn-danger gl-mr-3'
- = form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
- = submit_tag _('Regenerate recovery codes'), class: 'gl-button btn btn-default'
+ .js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
%p
@@ -53,6 +47,11 @@
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
+ .form-group
+ = label_tag :current_password, _('Current password'), class: 'label-bold'
+ = password_field_tag :current_password, nil, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
+ %p.form-text.text-muted
+ = _('Your current password is required to register a two-factor authenticator app.')
.gl-mt-3
= submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' }
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index eb4630b84d5..97f5cdb54e5 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -16,6 +16,7 @@
%li= _('Job logs and artifacts')
%li= _('Container registry images')
%li= _('CI variables')
+ %li= _('Pipeline triggers')
%li= _('Webhooks')
%li= _('Any encrypted tokens')
- if project.export_status == :finished
diff --git a/config/README.md b/config/README.md
index be5bd442fd8..52f9a244bd0 100644
--- a/config/README.md
+++ b/config/README.md
@@ -1,13 +1,13 @@
# Configuration files Documentation
Note that most configuration files (`config/*.*`) committed into
-[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-foss) **will not be used** for
+[gitlab-foss](https://gitlab.com/gitlab-org/gitlab-foss) **will not be used** for
[omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab). Configuration
-files committed into gitlab-ce are only used for development.
+files committed into gitlab-foss are only used for development.
## gitlab.yml
-You can find most of GitLab configuration settings here.
+You can find most of the GitLab configuration settings here.
## mail_room.yml
@@ -21,7 +21,7 @@ This file is called `resque.yml` for historical reasons. We are **NOT**
using Resque at the moment. It is used to specify Redis configuration
values when a single database instance of Redis is desired.
-# Advanced Redis configuration files
+## Advanced Redis configuration files
In more advanced configurations of Redis key-value storage, it is desirable
to separate the keys by lifecycle and intended use to ease provisioning and
@@ -40,7 +40,7 @@ If desired, the routing URL provided by these settings can be used with:
2. TCP port number for each Redis instance desired
3. `database number` for each Redis instance desired
-## Example URL attribute formats for GitLab Redis `.yml` configuration files
+### Example URL attribute formats for GitLab Redis `.yml` configuration files
* Unix Socket, default Redis database (0)
* `url: unix:/path/to/redis.sock`
* `url: unix:/path/to/redis.sock?db=`
@@ -52,129 +52,37 @@ If desired, the routing URL provided by these settings can be used with:
* TCP Socket for Redis on remote host `myserver`, port 6379, database 33
* `url: redis://:mynewpassword@myserver:6379/33`
-## redis.cache.yml
-
-If configured, `redis.cache.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for `Rails.cache` and other volatile non-persistent data which enhances
-the performance of GitLab.
-Settings here can be overridden by the environment variable
-`GITLAB_REDIS_CACHE_CONFIG_FILE` which provides
-an alternate location for configuration settings.
-
-The order of precedence for the URL used to connect to the Redis instance
-used for `cache` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CACHE_CONFIG_FILE` environment variable
-2. URL from `redis.cache.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6380`
-
-The order of precedence for all other configuration settings for `cache`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_CACHE_CONFIG_FILE` environment variable
-2. the configuration file `redis.cache.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
-
-## redis.queues.yml
-
-If configured, `redis.queues.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for clients of `::Gitlab::Redis::Queues`.
-These queues are intended to be the foundation
-of reliable inter-process communication between modules, whether on the same
-host node, or within a cluster. The primary clients of the queues are
-SideKiq, Mailroom, CI Runner, Workhorse, and push services. Settings here can
-be overridden by the environment variable
-`GITLAB_REDIS_QUEUES_CONFIG_FILE` which provides an alternate location for
-configuration settings.
-
-The order of precedence for the URL used to connect to the Redis instance
-used for `queues` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_QUEUES_CONFIG_FILE` environment variable
-2. URL from `redis.queues.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6381`
-
-The order of precedence for all other configuration settings for `queues`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_QUEUES_CONFIG_FILE` environment variable
-2. the configuration file `redis.queues.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
-
-## redis.shared_state.yml
-
-If configured, `redis.shared_state.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for clients of `::Gitlab::Redis::SharedState` such as session state,
-and rate limiting.
-Settings here can be overridden by the environment variable
-`GITLAB_REDIS_SHARED_STATE_CONFIG_FILE` which provides
-an alternate location for configuration settings.
-
-The order of precedence for the URL used to connect to the Redis instance
-used for `shared_state` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_SHARED_STATE_CONFIG_FILE` environment variable
-2. URL from `redis.shared_state.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6382`
-
-The order of precedence for all other configuration settings for `shared_state`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_SHARED_STATE_CONFIG_FILE` environment variable
-2. the configuration file `redis.shared_state.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
-
-## redis.trace_chunks.yml
-
-If configured, `redis.trace_chunks.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for clients of `::Gitlab::Redis::TraceChunks` which stores CI trace chunks.
-
-Settings here can be overridden by the environment variable
-`GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE` which provides
-an alternate location for configuration settings.
-
-The order of precedence for the URL used to connect to the Redis instance
-used for `trace_chunks` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE` environment variable
-2. URL from `redis.trace_chunks.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6383`
-
-The order of precedence for all other configuration settings for `trace_chunks`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE` environment variable
-2. the configuration file `redis.trace_chunks.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
+## Available configuration files
+
+The Redis instances that can be configured are described in the table below. The
+order of precedence for configuration is described below, where `$NAME` and
+`$FALLBACK_NAME` are the upper-cased instance names from the table, and `$name`
+and `$fallback_name` are the lower-cased versions:
+
+1. The configuration file pointed to by the `GITLAB_REDIS_$NAME_CONFIG_FILE`
+ environment variable.
+1. The configuration file `redis.$name.yml`.
+1. **If a fallback instance is available**, the configuration file
+ `redis.$fallback_name.yml`.
+1. The configuration file pointed to by the `GITLAB_REDIS_CONFIG_FILE`
+environment variable.
+1. The configuration file `resque.yml`.
+
+An example configuration file for Redis is in this directory under the name
+`resque.yml.example`.
+
+| Name | Fallback instance | Purpose |
+| --- | --- | --- |
+| `cache` | | Volatile non-persistent data |
+| `queues` | | Background job processing queues |
+| `shared_state` | | Persistent application state |
+| `trace_chunks` | `shared_state` | [CI trace chunks](https://docs.gitlab.com/ee/administration/job_logs.html#incremental-logging-architecture) |
+| `rate_limiting` | `cache` | [Rate limiting](https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html) state |
+
+If no configuration is found, or no URL is found in the configuration
+file, the default URL used is:
+
+1. `redis://localhost:6380` for `cache`.
+1. `redis://localhost:6381` for `queues`.
+1. `redis://localhost:6382` for `shared_state`.
+1. The URL from the fallback instance for all other instances.
diff --git a/config/application.rb b/config/application.rb
index 2349de4892f..83a3033a40d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -23,6 +23,8 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
+ require_dependency Rails.root.join('lib/gitlab/redis/trace_chunks')
+ require_dependency Rails.root.join('lib/gitlab/redis/rate_limiting')
require_dependency Rails.root.join('lib/gitlab/current_settings')
require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
require_dependency Rails.root.join('lib/gitlab/middleware/basic_health_check')
@@ -370,15 +372,7 @@ module Gitlab
end
# Use caching across all environments
- # Full list of options:
- # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
- caching_config_hash = {}
- caching_config_hash[:redis] = Gitlab::Redis::Cache.pool
- caching_config_hash[:compress] = Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1'))
- caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
- caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
-
- config.cache_store = :redis_cache_store, caching_config_hash
+ config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
config.active_job.queue_adapter = :sidekiq
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
index 84aa231089b..f861bf65367 100644
--- a/config/initializers/7_redis.rb
+++ b/config/initializers/7_redis.rb
@@ -11,3 +11,4 @@ Gitlab::Redis::Cache.with { nil }
Gitlab::Redis::Queues.with { nil }
Gitlab::Redis::SharedState.with { nil }
Gitlab::Redis::TraceChunks.with { nil }
+Gitlab::Redis::RateLimiting.with { nil }
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 477d419576a..25bf164c96a 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -51,6 +51,11 @@ Doorkeeper.configure do
# Issue access tokens with refresh token (disabled by default)
use_refresh_token
+ # Forbids creating/updating applications with arbitrary scopes that are
+ # not in configuration, i.e. `default_scopes` or `optional_scopes`.
+ # (disabled by default)
+ enforce_configured_scopes
+
# Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
# by default in non-development environments). OAuth2 delegates security in
# communication to the HTTPS protocol so it is wise to keep this enabled.
diff --git a/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
index 760fcba5935..1ede92609a9 100644
--- a/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
+++ b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
@@ -1,14 +1,46 @@
# frozen_string_literal: true
+# See https://github.com/omniauth/omniauth-oauth2/blob/v1.7.1/lib/omniauth/strategies/oauth2.rb#L84-L101
+# for the original version of this code.
+#
+# Note: We need to override `callback_phase` directly (instead of using a module with `include` or `prepend`),
+# because the method has a `super` call which needs to go to the `OmniAuth::Strategy` module,
+# and it also deletes `omniauth.state` from the session as a side effect.
+
module OmniAuth
module Strategies
class OAuth2
- alias_method :original_callback_phase, :callback_phase
-
- # Monkey patch until PR is merged and released upstream
- # https://github.com/omniauth/omniauth-oauth2/pull/129
def callback_phase
- original_callback_phase
+ error = request.params["error_reason"].presence || request.params["error"].presence
+ # Monkey patch #1:
+ #
+ # Swap the order of these conditions around so the `state` param is verified *first*,
+ # before using the error params returned by the provider.
+ #
+ # This avoids content spoofing attacks by crafting a URL with malicious messages,
+ # because the `state` param is only present in the session after a valid OAuth2 authentication flow.
+ if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
+ fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
+ elsif error
+ fail!(error, CallbackError.new(request.params["error"], request.params["error_description"].presence || request.params["error_reason"].presence, request.params["error_uri"]))
+ else
+ self.access_token = build_access_token
+ self.access_token = access_token.refresh! if access_token.expired?
+ super
+ end
+ rescue ::OAuth2::Error, CallbackError => e
+ fail!(:invalid_credentials, e)
+ rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
+ fail!(:timeout, e)
+ rescue ::SocketError => e
+ fail!(:failed_to_connect, e)
+ # Monkey patch #2:
+ #
+ # Also catch errors from Faraday.
+ # See https://github.com/omniauth/omniauth-oauth2/pull/129
+ # and https://github.com/oauth-xx/oauth2/issues/152
+ #
+ # This can be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/340933
rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed => e
fail!(:timeout, e)
end
diff --git a/config/redis.cache.yml.example b/config/redis.cache.yml.example
deleted file mode 100644
index 44d9f7e8632..00000000000
--- a/config/redis.cache.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/10
- #
- # url: redis://localhost:6380
- # sentinels:
- # -
- # host: localhost
- # port: 26380 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26380 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/10
- #
- # url: redis://localhost:6380
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.cache.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6380
- # sentinels:
- # -
- # host: replica1
- # port: 26380 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26380 # point to sentinel, not to redis port
diff --git a/config/redis.queues.yml.example b/config/redis.queues.yml.example
deleted file mode 100644
index 4194b44cb88..00000000000
--- a/config/redis.queues.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/11
- #
- # url: redis://localhost:6381
- # sentinels:
- # -
- # host: localhost
- # port: 26381 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26381 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/11
- #
- # url: redis://localhost:6381
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.queues.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6381
- # sentinels:
- # -
- # host: replica1
- # port: 26381 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26381 # point to sentinel, not to redis port
diff --git a/config/redis.shared_state.yml.example b/config/redis.shared_state.yml.example
deleted file mode 100644
index b3e0c7a8fa9..00000000000
--- a/config/redis.shared_state.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/12
- #
- # url: redis://localhost:6382
- # sentinels:
- # -
- # host: localhost
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/12
- #
- # url: redis://localhost:6382
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.shared_state.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6382
- # sentinels:
- # -
- # host: replica1
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
diff --git a/config/redis.trace_chunks.yml.example b/config/redis.trace_chunks.yml.example
deleted file mode 100644
index d38b9ba4966..00000000000
--- a/config/redis.trace_chunks.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/13
- #
- # url: redis://localhost:6382
- # sentinels:
- # -
- # host: localhost
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/13
- #
- # url: redis://localhost:6382
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.trace_chunks.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6382
- # sentinels:
- # -
- # host: replica1
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
diff --git a/db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb b/db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb
new file mode 100644
index 00000000000..4756bc3dca5
--- /dev/null
+++ b/db/post_migrate/20210914095310_cleanup_orphan_project_access_tokens.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class CleanupOrphanProjectAccessTokens < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ TMP_INDEX_NAME = 'idx_users_on_user_type_project_bots_batched'
+
+ def up
+ users_table = define_batchable_model('users')
+
+ add_concurrent_index(:users, :id, name: TMP_INDEX_NAME, where: 'user_type = 6')
+
+ accumulated_orphans = []
+ users_table.where(user_type: 6).each_batch(of: 500) do |relation|
+ orphan_ids = relation.where("not exists(select 1 from members where members.user_id = users.id)").pluck(:id)
+
+ orphan_ids.each_slice(10) do |ids|
+ users_table.where(id: ids).update_all(state: 'deactivated')
+ end
+
+ accumulated_orphans += orphan_ids
+ end
+
+ schedule_deletion(accumulated_orphans)
+ ensure
+ remove_concurrent_index_by_name(:users, TMP_INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:users, TMP_INDEX_NAME) if index_exists_by_name?(:users, TMP_INDEX_NAME)
+ end
+
+ private
+
+ def schedule_deletion(orphan_ids)
+ return unless deletion_worker
+
+ orphan_ids.each_slice(100) do |ids|
+ job_arguments = ids.map do |orphan_id|
+ [orphan_id, orphan_id, { skip_authorization: true }]
+ end
+
+ deletion_worker.bulk_perform_async(job_arguments)
+ end
+ rescue StandardError
+ # Ignore any errors or interface changes since this part of migration is optional
+ end
+
+ def deletion_worker
+ @deletion_worker = "DeleteUserWorker".safe_constantize unless defined?(@deletion_worker)
+
+ @deletion_worker
+ end
+end
diff --git a/db/schema_migrations/20210914095310 b/db/schema_migrations/20210914095310
new file mode 100644
index 00000000000..fee7e0b9719
--- /dev/null
+++ b/db/schema_migrations/20210914095310
@@ -0,0 +1 @@
+6fcf3ff9867df68f5e9603ae0311b29bec33aa5c5b826786b094ab0960ebcd90 \ No newline at end of file
diff --git a/doc/administration/configure.md b/doc/administration/configure.md
index d3e37b4a0ee..822acc1a74e 100644
--- a/doc/administration/configure.md
+++ b/doc/administration/configure.md
@@ -7,10 +7,44 @@ type: reference
# Configure your GitLab installation **(FREE SELF)**
-Customize and configure your self-managed GitLab installation.
+Customize and configure your self-managed GitLab installation. Here are some quick links to get you started:
- [Authentication](auth/index.md)
- [Configuration](../user/admin_area/index.md)
- [Repository storage](repository_storage_paths.md)
- [Geo](geo/index.md)
- [Packages](packages/index.md)
+
+The following tables are intended to guide you to choose the right combination of capabilties based on your requirements. It is common to want the most
+available, quickly recoverable, highly performant and fully data resilient solution. However, there are tradeoffs.
+
+The tables lists features on the left and provides their capabilities to the right along with known trade-offs.
+
+## Gitaly Capabilities
+
+| | Availability | Recoverability | Data Resiliency | Performance | Risks/Trade-offs|
+|-|--------------|----------------|-----------------|-------------|-----------------|
+|Gitaly Cluster | Very high - tolerant of node failures | RTO for a single node of 10s with no manual intervention | Data is stored on multiple nodes | Good - While writes may take slightly longer due to voting, read distribution improves read speeds | **Trade-off** - Slight decrease in write speed for redundant, strongly-consistent storage solution. **Risks** - [Does not currently support snapshot backups](gitaly/index.md#snapshot-backup-and-recovery-limitations), GitLab backup task can be slow for large data sets |
+|Gitaly Shards | Single storage location is a single point of failure | Would need to restore only shards which failed | Single point of failure | Good - can allocate repositories to shards to spread load | **Trade-off** - Need to manually configure repositories into different shards to balance loads / storage space **Risks** - Single point of failure relies on recovery process when single-node failure occurs |
+|Gitaly + NFS | Single storage location is a single point of failure | Single node failure requires restoration from backup | Single point of failure | Average - NFS is not ideally suited to large quantities of small reads / writes which can have a detrimental impact on performance | **Trade-off** - Easy and familiar administration though NFS is not ideally suited to Git demands **Risks** - Many instances of NFS compatibility issues which provide very poor customer experiences |
+
+## Geo Capabilities
+
+If your availabity needs to span multiple zones or multiple locations, please read about [Geo](geo/index.md).
+
+| | Availability | Recoverability | Data Resiliency | Performance | Risks/Trade-offs|
+|-|--------------|----------------|-----------------|-------------|-----------------|
+|Geo| Depends on the architecture of the Geo site. It is possible to deploy secondaries in single and multiple node configurations. | Eventually consistent. Recovery point depends on replication lag, which depends on a number of factors such as network speeds. Geo supports failover from a primary to secondary site using manual commands that are scriptable. | Geo currently replicates 100% of planned data types and verifies 50%. See [limitations table](geo/replication/datatypes.md#limitations-on-replicationverification) for more detail. | Improves read/clone times for users of a secondary. | Geo is not intended to replace other backup/restore solutions. Because of replication lag and the possibility of replicating bad data from a primary, we recommend that customers also take regular backups of their primary site and test the restore process. |
+
+## Scenarios for failure modes and available mitigation paths
+
+The following table outlines failure modes and mitigation paths for the product offerings detailed in the tables above. Note - Gitaly Cluster install assumes an odd number replication factor of 3 or greater
+
+| Gitaly Mode | Loss of Single Gitaly Node | Application / Data Corruption | Regional Outage (Loss of Instance) | Notes |
+| ----------- | -------------------------- | ----------------------------- | ---------------------------------- | ----- |
+| Single Gitaly Node | Downtime - Must restore from backup | Downtime - Must restore from Backup | Downtime - Must wait for outage to end | |
+| Single Gitaly Node + Geo Secondary | Downtime - Must restore from backup, can perform a manual failover to secondary | Downtime - Must restore from Backup, errors could have propagated to secondary | Manual intervention - failover to Geo secondary | |
+| Sharded Gitaly Install | Partial Downtime - Only repos on impacted node affected, must restore from backup | Partial Downtime - Only repos on impacted node affected, must restore from backup | Downtime - Must wait for outage to end | |
+| Sharded Gitaly Install + Geo Secondary | Partial Downtime - Only repos on impacted node affected, must restore from backup, could perform manual failover to secondary for impacted repos | Partial Downtime - Only repos on impacted node affected, must restore from backup, errors could have propagated to secondary | Manual intervention - failover to Geo secondary | |
+| Gitaly Cluster Install* | No Downtime - will swap repository primary to another node after 10 seconds | N/A - All writes are voted on by multiple Gitaly Cluster nodes | Downtime - Must wait for outage to end | Snapshot backups for Gitaly Cluster nodes not supported at this time |
+| Gitaly Cluster Install* + Geo Secondary | No Downtime - will swap repository primary to another node after 10 seconds | N/A - All writes are voted on by multiple Gitaly Cluster nodes | Manual intervention - failover to Geo secondary | Snapshot backups for Gitaly Cluster nodes not supported at this time |
diff --git a/doc/administration/gitaly/faq.md b/doc/administration/gitaly/faq.md
index c7ecaa020e0..f79b9555c10 100644
--- a/doc/administration/gitaly/faq.md
+++ b/doc/administration/gitaly/faq.md
@@ -35,7 +35,7 @@ For more information, see:
## Are there instructions for migrating to Gitaly Cluster?
-Yes! For more information, see [Migrate to Gitaly Cluster](index.md#migrate-to-gitaly-cluster).
+Yes! For more information, see [Migrating to Gitaly Cluster](index.md#migrating-to-gitaly-cluster).
## What are some repository storage recommendations?
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 85da6139f07..e15812a92cd 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -43,33 +43,49 @@ repository storage is either:
- Read requests are distributed between multiple Gitaly nodes, which can improve performance.
- Write requests are broadcast to repository replicas.
-WARNING:
-Engineering support for NFS for Git repositories is deprecated. Read the
-[deprecation notice](#nfs-deprecation-notice).
+## Guidance regarding Gitaly Cluster
-## Recommendations
+Gitaly Cluster provides the benefits of fault tolerance, but comes with additional complexity of setup and management. Please review existing technical limitations and considerations prior to deploying Gitaly Cluster.
-The following table provides recommendations based on your requirements. Users means concurrent
-users actively performing simultaneous Git operations.
+- [Known issues](#known-issues)
+- [Snapshot limitations](#snapshot-backup-and-recovery-limitations).
-| User scaling | Reference architecture to use | GitLab instance configuration | Gitaly configuration | Git repository storage | Instances dedicated to Gitaly services |
-|:-----------------------------------------------------------------------|:----------------------------------------------------------------|:---------------------------------------------|:--------------------------------|:-----------------------------------|:---------------------------------------|
-| Up to 1000 users | [1K](../reference_architectures/1k_users.md) | Single instance for all of GitLab | Already integrated as a service | Local disk | 0 |
-| Up to 2999 users | [2K](../reference_architectures/2k_users.md) | Horizontally-scaled GitLab instance (non-HA) | Single Gitaly server | Local disk of Gitaly instance | 1 |
-| 3000 users and over | [3K](../reference_architectures/3k_users.md) | Horizontally-scaled GitLab instance with HA | Gitaly Cluster | Local disk of each Gitaly instance | 8 |
-| RTO/RPO requirements for AZ failure tolerance regardless of user scale | [3K (with downscaling)](../reference_architectures/3k_users.md) | Custom (1) | Gitaly Cluster | Local disk of each Gitaly instance | 8 |
+Please also review the [configuration guidance](configure_gitaly.md) and [Repository storage options](../repository_storage_paths.md) to make sure that Gitaly Cluster is the best set-up for you. Finally, refer to the following guidance:
-1. If you need AZ failure tolerance for user scaling lower than 3K, please contact Customer Success
- to discuss your RTO and RPO needs and what options exist to meet those objectives.
+- If you have not yet migrated to Gitaly Cluster and want to continue using NFS, remain on the
+ service you are using. NFS is supported in 14.x releases.
+- If you have not yet migrated to Gitaly Cluster but want to migrate away from NFS, you have two options - a sharded Gitaly instance or Gitaly Cluster.
+- If you have migrated to Gitaly Cluster and the limitations and tradeoffs are not suitable for your environment, your options are:
+ 1. [Migrate off Gitaly Cluster](#migrate-off-gitaly-cluster) back to your NFS solution
+ 1. [Migrate off Gitaly Cluster](#migrate-off-gitaly-cluster) to NFS solution or to a sharded Gitaly instance.
-GitLab [reference architectures](../reference_architectures/index.md) provide guidance for what
-Gitaly configuration is recommended at each scale. The Gitaly configuration is noted by the
-architecture diagrams and the table of required resources.
+Reach out to your Technical Account Manager or customer support if you have any questions.
-WARNING:
-Some [known database inconsistency issues](#known-issues) exist in Gitaly Cluster. We recommend you
-remain on your current service for now. We can adjust the date for
-[NFS support removal](#nfs-deprecation-notice) if this applies to you.
+### Known issues
+
+The following table outlines current known issues impacting the use of Gitaly Cluster. For
+the current status of these issues, please refer to the referenced issues and epics.
+
+| Issue | Summary | How to avoid |
+|:--------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------|
+| Gitaly Cluster + Geo - Issues retrying failed syncs | If Gitaly Cluster is used on a Geo secondary site, repositories that have failed to sync could continue to fail when Geo tries to resync them. Recovering from this state requires assistance from support to run manual steps. Work is in-progress to update Gitaly Cluster to [identify repositories with a unique and persistent identifier](https://gitlab.com/gitlab-org/gitaly/-/issues/3485), which is expected to resolve the issue. | No known solution at this time. |
+| Database inconsistencies due to repository access outside of Gitaly Cluster's control | Operations that write to the repository storage that do not go through normal Gitaly Cluster methods can cause database inconsistencies. These can include (but are not limited to) snapshot restoration for cluster node disks, node upgrades which modify files under Git control, or any other disk operation that may touch repository storage external to GitLab. The Gitaly team is actively working to provide manual commands to [reconcile the Praefect database with the repository storage](https://gitlab.com/groups/gitlab-org/-/epics/6723). | Don't directly change repositories on any Gitaly Cluster node at this time. |
+| Praefect unable to insert data into the database due to migrations not being applied after an upgrade | If the database is not kept up to date with completed migrations, then the Praefect node is unable to perform normal operation. | Make sure the Praefect database is up and running with all migrations completed (For example: `/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml sql-migrate-status` should show a list of all applied migrations). Consider [requesting live upgrade assistance](https://about.gitlab.com/support/scheduling-live-upgrade-assistance.html) so your upgrade plan can be reviewed by support. |
+| Restoring a Gitaly Cluster node from a snapshot in a running cluster | Because the Gitaly Cluster runs with consistent state, introducing a single node that is behind will result in the cluster not being able to reconcile the nodes data and other nodes data | Don't restore a single Gitaly Cluster node from a backup snapshot. If you need to restore from backup, it's best to snapshot all Gitaly Cluster nodes at the same time and take a database dump of the Praefect database. |
+
+### Snapshot backup and recovery limitations
+
+Gitaly Cluster does not support snapshot backups because these can cause issues where the Praefect
+database becomes out of sync with the disk storage. Because of how Praefect rebuilds the replication
+metadata of Gitaly disk information during a restore, we recommend using the
+[official backup and restore Rake tasks](../../raketasks/backup_restore.md). If you are unable to use this method, please contact customer support for restoration help.
+
+To track progress on work on a solution for manually re-synchronizing the Praefect database with
+disk storage, see [this epic](https://gitlab.com/groups/gitlab-org/-/epics/6575).
+
+### What to do if you are on Gitaly Cluster experiencing an issue or limitation
+
+Please contact customer support for immediate help in restoration or recovery.
## Gitaly
@@ -188,26 +204,6 @@ WARNING:
If complete cluster failure occurs, disaster recovery plans should be executed. These can affect the
RPO and RTO discussed above.
-### Known issues
-
-The following table outlines current known issues impacting the use of Gitaly Cluster. For
-the current status of these issues, please refer to the referenced issues and epics.
-
-| Issue | Summary |
-|:--------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Gitaly Cluster + Geo can cause database inconsistencies | There are some conditions during Geo replication that can cause database inconsistencies with Gitaly Cluster. These have been identified and are being resolved by updating Gitaly Cluster to [identify repositories with a unique and persistent identifier](https://gitlab.com/gitlab-org/gitaly/-/issues/3485). |
-| Database inconsistencies due to repository access outside of Gitaly Cluster's control | Operations that write to the repository storage that do not go through normal Gitaly Cluster methods can cause database inconsistencies. These can include (but are not limited to) snapshot restoration for cluster node disks, node upgrades which modify files under Git control, or any other disk operation that may touch repository storage external to GitLab. The Gitaly team is actively working to provide manual commands to [reconcile the Praefect database with the repository storage](https://gitlab.com/groups/gitlab-org/-/epics/6723). |
-
-### Snapshot backup and recovery limitations
-
-Gitaly Cluster does not support snapshot backups because these can cause issues where the Praefect
-database becomes out of sync with the disk storage. Because of how Praefect rebuilds the replication
-metadata of Gitaly disk information during a restore, we recommend using the
-[official backup and restore Rake tasks](../../raketasks/backup_restore.md).
-
-To track progress on work on a solution for manually re-synchronizing the Praefect database with
-disk storage, see [this epic](https://gitlab.com/groups/gitlab-org/-/epics/6575).
-
### Virtual storage
Virtual storage makes it viable to have a single repository storage in GitLab to simplify repository
@@ -236,9 +232,7 @@ As with normal Gitaly storages, virtual storages can be sharded.
### Moving beyond NFS
-WARNING:
-Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be
-unavailable from GitLab 15.0. No further enhancements are planned for this feature.
+Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be unavailable starting GitLab 15.0. Please see our [statement of support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs) for more details.
[Network File System (NFS)](https://en.wikipedia.org/wiki/Network_File_System)
is not well suited to Git workloads which are CPU and IOPS sensitive.
@@ -359,13 +353,9 @@ For configuration information, see [Configure replication factor](praefect.md#co
For more information on configuring Gitaly Cluster, see [Configure Gitaly Cluster](praefect.md).
-## Migrate to Gitaly Cluster
+## Migrating to Gitaly Cluster
-We recommend you migrate to Gitaly Cluster if your [requirements recommend](#recommendations) Gitaly
-Cluster.
-
-Whether migrating to Gitaly Cluster because of [NFS support deprecation](index.md#nfs-deprecation-notice)
-or to move from single Gitaly nodes, the basic process involves:
+Please see [current guidance on Gitaly Cluster](#guidance-regarding-gitaly-cluster). The basic process for migrating to Gitaly Cluster involves:
1. Create the required storage. Refer to
[repository storage recommendations](faq.md#what-are-some-repository-storage-recommendations).
@@ -376,8 +366,7 @@ or to move from single Gitaly nodes, the basic process involves:
WARNING:
Some [known database inconsistency issues](#known-issues) exist in Gitaly Cluster. We recommend you
-remain on your current service for now. We can adjust the date for
-[NFS support removal](#nfs-deprecation-notice) if this applies to you.
+remain on your current service for now.
### Migrate off Gitaly Cluster
@@ -631,20 +620,4 @@ The second facet presents the only real solution. For this, we developed
## NFS deprecation notice
Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be
-unavailable from GitLab 15.0. No further enhancements are planned for this feature.
-
-Additional information:
-
-- [Recommended NFS mount options and known issues with Gitaly and NFS](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
-- [GitLab statement of support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs).
-
-GitLab recommends:
-
-- Creating a [Gitaly Cluster](#gitaly-cluster) as soon as possible.
-- [Moving your repositories](#migrate-to-gitaly-cluster) from NFS-based storage to Gitaly
- Cluster.
-
-We welcome your feedback on this process. You can:
-
-- Raise a support ticket.
-- [Comment on the epic](https://gitlab.com/groups/gitlab-org/-/epics/4916).
+unavailable from GitLab 15.0. For further information, please see our [NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation) documentation.
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 81eb36d38ab..0d85bb76855 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -429,7 +429,7 @@ On the **Praefect** node:
WARNING:
If you have data on an already existing storage called
`default`, you should configure the virtual storage with another name and
- [migrate the data to the Gitaly Cluster storage](index.md#migrate-to-gitaly-cluster)
+ [migrate the data to the Gitaly Cluster storage](index.md#migrating-to-gitaly-cluster)
afterwards.
Replace `PRAEFECT_INTERNAL_TOKEN` with a strong secret, which is used by
@@ -893,7 +893,7 @@ Particular attention should be shown to:
WARNING:
If you have existing data stored on the default Gitaly storage,
- you should [migrate the data your Gitaly Cluster storage](index.md#migrate-to-gitaly-cluster)
+ you should [migrate the data your Gitaly Cluster storage](index.md#migrating-to-gitaly-cluster)
first.
```ruby
diff --git a/doc/administration/nfs.md b/doc/administration/nfs.md
index f1a02c66211..ec4e515f12e 100644
--- a/doc/administration/nfs.md
+++ b/doc/administration/nfs.md
@@ -20,12 +20,46 @@ file system performance, see
## Gitaly and NFS deprecation
+Starting with GitLab version 14.0, support for NFS to store Git repository data will be deprecated. Technical customer support and engineering support will be available for the 14.x releases. Engineering will fix bugs and security vulnerabilities consistent with our [release and maintenance policy](../policy/maintenance.md#security-releases).
+
+At the end of the 14.12 milestone (tenatively June 22nd, 2022) technical and engineering support for using NFS to store Git repository data will be officially at end-of-life. There will be no product changes or troubleshooting provided via Engineering, Security or Paid Support channels.
+
+For those customers still running earlier versions of GitLab, [our support eligibility and maintenance policy applies](https://about.gitlab.com/support/statement-of-support.html#version-support).
+
+For the 14.x releases, we will continue to help with Git related tickets from customers running one or more Gitaly servers with its data stored on NFS. Examples may include:
+
+- Performance issues or timeouts accessing Git data
+- Commits or branches vanish
+- GitLab intermittently returns the wrong Git data (such as reporting that a repository has no branches)
+
+Assistance will be limited to activities like:
+
+- Verifying developers' workflow uses features like protected branches
+- Reviewing GitLab event data from the database to advise if it looks like a force push over-wrote branches
+- Verifying that NFS client mount options match our [documented recommendations](#mount-options)
+- Analyzing the GitLab Workhorse and Rails logs, and determining that `500` errors being seen in the environment are caused by slow responses from Gitaly
+
+GitLab support will be unable to continue with the investigation if:
+
+- The date of the request is on or after the release of GitLab version 15.0, and
+- Support Engineers and Management determine that all reasonable non-NFS root causes have been exhausted
+
+If the issue is reproducible, or if it happens intermittently but regularly, GitLab Support will investigate providing the issue reproduces without the use of NFS. In order to reproduce without NFS, the affected repositories should be migrated to a different Gitaly shard, such as Gitaly cluster or a standalone Gitaly VM, backed with block storage.
+
+### Why remove NFS for Git repository data
+
+{:.no-toc}
+
+NFS is not well-suited to a workload consisting of many small files, like Git repositories. NFS does provide a number of configuration options designed to improve performance. However, over time, a number of these mount options have proven to result in inconsistencies across multiple nodes mounting the NFS volume, up to and including data loss. Addressing these inconsistencies consume extraordinary development and support engineer time that hamper our ability to develop [Gitaly Cluster](gitaly/praefect.md), our purpose-built solution to addressing the deficiencies of NFS in this environment.
+
+Please note that Gitaly Cluster provides highly-available Git repository storage. If this is not a requirement, single-node Gitaly backed by block storage is a suitable substitute.
+
Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be
unavailable from GitLab 15.0. No further enhancements are planned for this feature.
Read:
-- The [Gitaly and NFS deprecation notice](gitaly/index.md#nfs-deprecation-notice).
+- [Moving beyond NFS](gitaly/index.md#moving-beyond-nfs).
- About the [correct mount options to use](#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
## Known kernel version incompatibilities
@@ -370,8 +404,8 @@ sudo ufw allow from <client_ip_address> to any port nfs
### Upgrade to Gitaly Cluster or disable caching if experiencing data loss
WARNING:
-Engineering support for NFS for Git repositories is deprecated. Read the
-[Gitaly and NFS deprecation notice](gitaly/index.md#nfs-deprecation-notice).
+Engineering support for NFS for Git repositories is deprecated. Read about
+[moving beyond NFS](gitaly/index.md#moving-beyond-nfs).
Customers and users have reported data loss on high-traffic repositories when using NFS for Git repositories.
For example, we have seen:
diff --git a/doc/administration/operations/moving_repositories.md b/doc/administration/operations/moving_repositories.md
index 61a9ec8e7d4..8aeaadc17e9 100644
--- a/doc/administration/operations/moving_repositories.md
+++ b/doc/administration/operations/moving_repositories.md
@@ -27,7 +27,7 @@ For more information, see:
querying and scheduling snippet repository moves.
- [The API documentation](../../api/group_repository_storage_moves.md) details the endpoints for
querying and scheduling group repository moves **(PREMIUM SELF)**.
-- [Migrate to Gitaly Cluster](../gitaly/index.md#migrate-to-gitaly-cluster).
+- [Migrating to Gitaly Cluster](../gitaly/index.md#migrating-to-gitaly-cluster).
### Move Repositories
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index 1067474c8b4..8c6adcee055 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -151,6 +151,19 @@ otherwise conflicts occur.
If your certificate provider provides the CA Bundle certificates, append them to the TLS certificate file.
+An administrator may want the container registry listening on an arbitrary port such as `5678`.
+However, the registry and application server are behind an AWS application load balancer that only
+listens on ports `80` and `443`. The admin may simply remove the port number for
+`registry_external_url`, so HTTP or HTTPS is assumed. Then, the rules apply that map the load
+balancer to the registry from ports `80` or `443` to the arbitrary port. This is important if users
+rely on the `docker login` example in the container registry. Here's an example:
+
+```ruby
+registry_external_url 'https://registry-gitlab.example.com'
+registry_nginx['redirect_http_to_https'] = true
+registry_nginx['listen_port'] = 5678
+```
+
**Installations from source**
1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
diff --git a/doc/administration/redis/replication_and_failover.md b/doc/administration/redis/replication_and_failover.md
index 48db734b1af..db652a80780 100644
--- a/doc/administration/redis/replication_and_failover.md
+++ b/doc/administration/redis/replication_and_failover.md
@@ -647,6 +647,7 @@ persistence classes.
| `shared_state` | Store session-related and other persistent data. |
| `actioncable` | Pub/Sub queue backend for ActionCable. |
| `trace_chunks` | Store [CI trace chunks](../job_logs.md#enable-or-disable-incremental-logging) data. |
+| `rate_limiting` | Store [rate limiting](../../user/admin_area/settings/user_and_ip_rate_limits.md) state. |
To make this work with Sentinel:
@@ -659,6 +660,7 @@ To make this work with Sentinel:
gitlab_rails['redis_shared_state_instance'] = REDIS_SHARED_STATE_URL
gitlab_rails['redis_actioncable_instance'] = REDIS_ACTIONCABLE_URL
gitlab_rails['redis_trace_chunks_instance'] = REDIS_TRACE_CHUNKS_URL
+ gitlab_rails['redis_rate_limiting_instance'] = REDIS_RATE_LIMITING_URL
# Configure the Sentinels
gitlab_rails['redis_cache_sentinels'] = [
@@ -681,6 +683,10 @@ To make this work with Sentinel:
{ host: TRACE_CHUNKS_SENTINEL_HOST, port: 26379 },
{ host: TRACE_CHUNKS_SENTINEL_HOST2, port: 26379 }
]
+ gitlab_rails['redis_rate_limiting_sentinels'] = [
+ { host: RATE_LIMITING_SENTINEL_HOST, port: 26379 },
+ { host: RATE_LIMITING_SENTINEL_HOST2, port: 26379 }
+ ]
```
- Redis URLs should be in the format: `redis://:PASSWORD@SENTINEL_PRIMARY_NAME`, where:
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index d107c40190b..b16c3dd11ba 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -2118,7 +2118,7 @@ unavailable from GitLab 15.0. No further enhancements are planned for this featu
Read:
-- The [Gitaly and NFS deprecation notice](../gitaly/index.md#nfs-deprecation-notice).
+- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation).
- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
<div align="right">
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index ba6d6e1407b..94c51267d77 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -2124,7 +2124,7 @@ unavailable from GitLab 15.0. No further enhancements are planned for this featu
Read:
-- The [Gitaly and NFS deprecation notice](../gitaly/index.md#nfs-deprecation-notice).
+- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation).
- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
## Cloud Native Hybrid reference architecture with Helm Charts (alternative)
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index 47d83c46c06..8022d8e9086 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -965,7 +965,7 @@ unavailable from GitLab 15.0. No further enhancements are planned for this featu
Read:
-- The [Gitaly and NFS deprecation notice](../gitaly/index.md#nfs-deprecation-notice).
+- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation).
- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
## Cloud Native Hybrid reference architecture with Helm Charts (alternative)
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 60365bb7ee0..f60630f5856 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -2072,7 +2072,7 @@ unavailable from GitLab 15.0. No further enhancements are planned for this featu
Read:
-- The [Gitaly and NFS deprecation notice](../gitaly/index.md#nfs-deprecation-notice).
+- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation).
- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
## Supported modifications for lower user counts (HA)
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 913fde9d528..8c51f4a71a4 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -2138,7 +2138,7 @@ unavailable from GitLab 15.0. No further enhancements are planned for this featu
Read:
-- The [Gitaly and NFS deprecation notice](../gitaly/index.md#nfs-deprecation-notice).
+- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation).
- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
## Cloud Native Hybrid reference architecture with Helm Charts (alternative)
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index 071a90a4c00..6377f6ae62d 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -2066,7 +2066,7 @@ unavailable from GitLab 15.0. No further enhancements are planned for this featu
Read:
-- The [Gitaly and NFS deprecation notice](../gitaly/index.md#nfs-deprecation-notice).
+- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation).
- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
## Cloud Native Hybrid reference architecture with Helm Charts (alternative)
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index 6d328739f3e..ed50d0e7263 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -174,4 +174,4 @@ information.
## Move repositories
To move a repository to a different repository storage (for example, from `default` to `storage2`), use the
-same process as [migrating to Gitaly Cluster](gitaly/index.md#migrate-to-gitaly-cluster).
+same process as [migrating to Gitaly Cluster](gitaly/index.md#migrating-to-gitaly-cluster).
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 8706b1e7e76..7dc3fd1db21 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -42,6 +42,7 @@ The following API resources are available in the project context:
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` |
+| [Integrations](integrations.md) | `/projects/:id/integrations` |
| [Invitations](invitations.md) | `/projects/:id/invitations` (also available for groups) |
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
@@ -82,7 +83,7 @@ The following API resources are available in the project context:
| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` (also available for groups) |
| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) |
| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
-| [Services](services.md) | `/projects/:id/services` |
+| [Services](services.md) (renamed to [Integrations](integrations.md)) | `/projects/:id/services` |
| [Tags](tags.md) | `/projects/:id/repository/tags` |
| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` |
| [Visual Review discussions](visual_review_discussions.md) **(PREMIUM)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md
index c8b928ab5b2..6e9c37980ac 100644
--- a/doc/api/dependencies.md
+++ b/doc/api/dependencies.md
@@ -11,6 +11,9 @@ This API is in an alpha stage and considered unstable.
The response payload may be subject to change or breakage
across GitLab releases.
+> - Introduced in GitLab 12.1.
+> - Pagination introduced in 14.4.
+
Every call to this endpoint requires authentication. To perform this call, user should be authorized to read repository.
To see vulnerabilities in response, user should be authorized to read
[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard).
@@ -60,3 +63,10 @@ Example response:
}
]
```
+
+## Dependencies pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](index.md#pagination).
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4e69179e1a4..b180df9c6f3 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -16493,6 +16493,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumregistration_enabled_callout"></a>`REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. |
| <a id="usercalloutfeaturenameenumsecurity_configuration_devops_alert"></a>`SECURITY_CONFIGURATION_DEVOPS_ALERT` | Callout feature name for security_configuration_devops_alert. |
| <a id="usercalloutfeaturenameenumsecurity_configuration_upgrade_banner"></a>`SECURITY_CONFIGURATION_UPGRADE_BANNER` | Callout feature name for security_configuration_upgrade_banner. |
+| <a id="usercalloutfeaturenameenumsecurity_newsletter_callout"></a>`SECURITY_NEWSLETTER_CALLOUT` | Callout feature name for security_newsletter_callout. |
| <a id="usercalloutfeaturenameenumsuggest_pipeline"></a>`SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. |
| <a id="usercalloutfeaturenameenumsuggest_popover_dismissed"></a>`SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. |
| <a id="usercalloutfeaturenameenumtabs_position_highlight"></a>`TABS_POSITION_HIGHLIGHT` | Callout feature name for tabs_position_highlight. |
diff --git a/doc/api/group_repository_storage_moves.md b/doc/api/group_repository_storage_moves.md
index a893bffb1f5..9d4120ec355 100644
--- a/doc/api/group_repository_storage_moves.md
+++ b/doc/api/group_repository_storage_moves.md
@@ -10,7 +10,7 @@ type: reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53016) in GitLab 13.9.
Group repositories can be moved between storages. This API can help you when
-[migrating to Gitaly Cluster](../administration/gitaly/index.md#migrate-to-gitaly-cluster), for
+[migrating to Gitaly Cluster](../administration/gitaly/index.md#migrating-to-gitaly-cluster), for
example, or to migrate a [group wiki](../user/project/wiki/index.md#group-wikis).
As group repository storage moves are processed, they transition through different states. Values
diff --git a/doc/api/integrations.md b/doc/api/integrations.md
new file mode 100644
index 00000000000..75e5047f395
--- /dev/null
+++ b/doc/api/integrations.md
@@ -0,0 +1,1473 @@
+---
+stage: Ecosystem
+group: Integrations
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Integrations API **(FREE)**
+
+This API enables you to work with external services that integrate with GitLab.
+
+NOTE:
+In GitLab 14.4, the `services` endpoint was [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/334500) to `integrations`.
+Calls to the Integrations API can be made to both `/projects/:id/services` and `/projects/:id/integrations`.
+The examples in this document refer to the endpoint at `/projects/:id/integrations`.
+
+This API requires an access token with the [Maintainer or Owner role](../user/permissions.md).
+
+## List all active integrations
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21330) in GitLab 12.7.
+
+Get a list of all active project integrations.
+
+```plaintext
+GET /projects/:id/integrations
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 75,
+ "title": "Jenkins CI",
+ "slug": "jenkins",
+ "created_at": "2019-11-20T11:20:25.297Z",
+ "updated_at": "2019-11-20T12:24:37.498Z",
+ "active": true,
+ "commit_events": true,
+ "push_events": true,
+ "issues_events": true,
+ "confidential_issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": false,
+ "note_events": true,
+ "confidential_note_events": true,
+ "pipeline_events": true,
+ "wiki_page_events": true,
+ "job_events": true,
+ "comment_on_event_enabled": true
+ },
+ {
+ "id": 76,
+ "title": "Alerts endpoint",
+ "slug": "alerts",
+ "created_at": "2019-11-20T11:20:25.297Z",
+ "updated_at": "2019-11-20T12:24:37.498Z",
+ "active": true,
+ "commit_events": true,
+ "push_events": true,
+ "issues_events": true,
+ "confidential_issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "confidential_note_events": true,
+ "pipeline_events": true,
+ "wiki_page_events": true,
+ "job_events": true,
+ "comment_on_event_enabled": true
+ }
+]
+```
+
+## Asana
+
+Add commit messages as comments to Asana tasks.
+
+See also the [Asana integration documentation](../user/project/integrations/asana.md).
+
+### Create/Edit Asana integration
+
+Set Asana integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/asana
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_key` | string | true | User API token. User must have access to task. All comments are attributed to this user. |
+| `restrict_to_branch` | string | false | Comma-separated list of branches to be are automatically inspected. Leave blank to include all branches. |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Asana integration
+
+Delete Asana integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/asana
+```
+
+### Get Asana integration settings
+
+Get Asana integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/asana
+```
+
+## Assembla
+
+Project Management Software (Source Commits Endpoint)
+
+### Create/Edit Assembla integration
+
+Set Assembla integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/assembla
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | The authentication token
+| `subdomain` | string | false | The subdomain setting |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Assembla integration
+
+Delete Assembla integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/assembla
+```
+
+### Get Assembla integration settings
+
+Get Assembla integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/assembla
+```
+
+## Atlassian Bamboo CI
+
+A continuous integration and build server
+
+### Create/Edit Atlassian Bamboo CI integration
+
+Set Atlassian Bamboo CI integration for a project.
+
+> You must set up automatic revision labeling and a repository trigger in Bamboo.
+
+```plaintext
+PUT /projects/:id/integrations/bamboo
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `bamboo_url` | string | true | Bamboo root URL. For example, `https://bamboo.example.com`. |
+| `build_key` | string | true | Bamboo build plan key like KEY |
+| `username` | string | true | A user with API access, if applicable |
+| `password` | string | true | Password of the user |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Atlassian Bamboo CI integration
+
+Delete Atlassian Bamboo CI integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/bamboo
+```
+
+### Get Atlassian Bamboo CI integration settings
+
+Get Atlassian Bamboo CI integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/bamboo
+```
+
+## Bugzilla
+
+Bugzilla Issue Tracker
+
+### Create/Edit Bugzilla integration
+
+Set Bugzilla integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/bugzilla
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `new_issue_url` | string | true | New Issue URL |
+| `issues_url` | string | true | Issue URL |
+| `project_url` | string | true | Project URL |
+| `description` | string | false | Description |
+| `title` | string | false | Title |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Bugzilla integration
+
+Delete Bugzilla integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/bugzilla
+```
+
+### Get Bugzilla integration settings
+
+Get Bugzilla integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/bugzilla
+```
+
+## Buildkite
+
+Continuous integration and deployments
+
+### Create/Edit Buildkite integration
+
+Set Buildkite integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/buildkite
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Buildkite project GitLab token |
+| `project_url` | string | true | Pipeline URL. For example, `https://buildkite.com/example/pipeline` |
+| `enable_ssl_verification` | boolean | false | DEPRECATED: This parameter has no effect since SSL verification is always enabled |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Buildkite integration
+
+Delete Buildkite integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/buildkite
+```
+
+### Get Buildkite integration settings
+
+Get Buildkite integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/buildkite
+```
+
+## Campfire
+
+Send notifications about push events to Campfire chat rooms.
+[New users can no longer sign up for Campfire](https://basecamp.com/retired/campfire).
+
+### Create/Edit Campfire integration
+
+Set Campfire integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/campfire
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+|---------------|---------|----------|---------------------------------------------------------------------------------------------|
+| `token` | string | true | Campfire API token. To find it, log into Campfire and select **My info**. |
+| `subdomain` | string | false | Campfire subdomain. Text between `https://` and `.campfirenow.com` when you're logged in. |
+| `room` | string | false | Campfire room. The last part of the URL when you're in a room. |
+| `push_events` | boolean | false | Enable notifications for push events. |
+
+### Delete Campfire integration
+
+Delete Campfire integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/campfire
+```
+
+### Get Campfire integration settings
+
+Get Campfire integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/campfire
+```
+
+## Unify Circuit
+
+Unify Circuit RTC and collaboration tool.
+
+### Create/Edit Unify Circuit integration
+
+Set Unify Circuit integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/unify-circuit
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Unify Circuit webhook. For example, `https://circuit.com/rest/v2/webhooks/incoming/...`. |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `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" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Unify Circuit integration
+
+Delete Unify Circuit integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/unify-circuit
+```
+
+### Get Unify Circuit integration settings
+
+Get Unify Circuit integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/unify-circuit
+```
+
+## Webex Teams
+
+Webex Teams collaboration tool.
+
+### Create/Edit Webex Teams integration
+
+Set Webex Teams integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/webex-teams
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Webex Teams webhook. For example, `https://api.ciscospark.com/v1/webhooks/incoming/...`. |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `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" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Webex Teams integration
+
+Delete Webex Teams integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/webex-teams
+```
+
+### Get Webex Teams integration settings
+
+Get Webex Teams integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/webex-teams
+```
+
+## Custom Issue Tracker
+
+Custom issue tracker
+
+### Create/Edit Custom Issue Tracker integration
+
+Set Custom Issue Tracker integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/custom-issue-tracker
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `new_issue_url` | string | true | New Issue URL |
+| `issues_url` | string | true | Issue URL |
+| `project_url` | string | true | Project URL |
+| `description` | string | false | Description |
+| `title` | string | false | Title |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Custom Issue Tracker integration
+
+Delete Custom Issue Tracker integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/custom-issue-tracker
+```
+
+### Get Custom Issue Tracker integration settings
+
+Get Custom Issue Tracker integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/custom-issue-tracker
+```
+
+## Drone CI
+
+Drone is a Continuous Integration platform built on Docker, written in Go
+
+### Create/Edit Drone CI integration
+
+Set Drone CI integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/drone-ci
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Drone CI project specific token |
+| `drone_url` | string | true | `http://drone.example.com` |
+| `enable_ssl_verification` | boolean | false | Enable SSL verification |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+
+### Delete Drone CI integration
+
+Delete Drone CI integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/drone-ci
+```
+
+### Get Drone CI integration settings
+
+Get Drone CI integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/drone-ci
+```
+
+## Emails on Push
+
+Email the commits and diff of each push to a list of recipients.
+
+### Create/Edit Emails on Push integration
+
+Set Emails on Push integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/emails-on-push
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | true | Emails separated by whitespace |
+| `disable_diffs` | boolean | false | Disable code diffs |
+| `send_from_committer_email` | boolean | false | Send from committer |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected". Notifications are always fired for tag pushes. The default value is "all" |
+
+### Delete Emails on Push integration
+
+Delete Emails on Push integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/emails-on-push
+```
+
+### Get Emails on Push integration settings
+
+Get Emails on Push integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/emails-on-push
+```
+
+## Confluence integration
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220934) in GitLab 13.2.
+
+Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace.
+
+### Create/Edit Confluence integration
+
+Set Confluence integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/confluence
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `confluence_url` | string | true | The URL of the Confluence Cloud Workspace hosted on atlassian.net. |
+
+### Delete Confluence integration
+
+Delete Confluence integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/confluence
+```
+
+### Get Confluence integration settings
+
+Get Confluence integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/confluence
+```
+
+## External wiki
+
+Replaces the link to the internal wiki with a link to an external wiki.
+
+### Create/Edit External wiki integration
+
+Set External wiki integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/external-wiki
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `external_wiki_url` | string | true | The URL of the external wiki |
+
+### Delete External wiki integration
+
+Delete External wiki integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/external-wiki
+```
+
+### Get External wiki integration settings
+
+Get External wiki integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/external-wiki
+```
+
+## Flowdock
+
+Flowdock is a ChatOps application for collaboration in software engineering
+companies. You can send notifications from GitLab events to Flowdock flows.
+For integration instructions, see the [Flowdock documentation](https://www.flowdock.com/help/gitlab).
+
+### Create/Edit Flowdock integration
+
+Set Flowdock integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/flowdock
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Flowdock Git source token |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Flowdock integration
+
+Delete Flowdock integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/flowdock
+```
+
+### Get Flowdock integration settings
+
+Get Flowdock integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/flowdock
+```
+
+## GitHub **(PREMIUM)**
+
+Code collaboration software.
+
+### Create/Edit GitHub integration
+
+Set GitHub integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/github
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | GitHub API token with `repo:status` OAuth scope |
+| `repository_url` | string | true | GitHub repository URL |
+| `static_context` | boolean | false | Append instance name instead of branch to [status check name](../user/project/integrations/github.md#static--dynamic-status-check-names) |
+
+### Delete GitHub integration
+
+Delete GitHub integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/github
+```
+
+### Get GitHub integration settings
+
+Get GitHub integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/github
+```
+
+## Hangouts Chat
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20290) in GitLab 11.2.
+
+Google Workspace team collaboration tool.
+
+### Create/Edit Hangouts Chat integration
+
+Set Hangouts Chat integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/hangouts-chat
+```
+
+NOTE:
+Specific event parameters (for example, `push_events` flag) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Hangouts Chat webhook. For example, `https://chat.googleapis.com/v1/spaces...`. |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
+| `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" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Hangouts Chat integration
+
+Delete Hangouts Chat integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/hangouts-chat
+```
+
+### Get Hangouts Chat integration settings
+
+Get Hangouts Chat integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/hangouts-chat
+```
+
+## Irker (IRC gateway)
+
+Send IRC messages, on update, to a list of recipients through an irker gateway.
+
+For more information, see the [irker integration documentation](../user/project/integrations/irker.md).
+
+### Create/Edit Irker (IRC gateway) integration
+
+Set Irker (IRC gateway) integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/irker
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | true | Recipients/channels separated by whitespaces |
+| `default_irc_uri` | string | false | `irc://irc.network.net:6697/` |
+| `server_host` | string | false | localhost |
+| `server_port` | integer | false | 6659 |
+| `colorize_messages` | boolean | false | Colorize messages |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Irker (IRC gateway) integration
+
+Delete Irker (IRC gateway) integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/irker
+```
+
+### Get Irker (IRC gateway) integration settings
+
+Get Irker (IRC gateway) integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/irker
+```
+
+## Jira
+
+Jira issue tracker.
+
+### Get Jira integration settings
+
+Get Jira integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/jira
+```
+
+### Create/Edit Jira integration
+
+Set Jira integration for a project.
+
+> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and
+> `project_url` are replaced by `url`. If you are using an
+> older version, [follow this documentation](https://gitlab.com/gitlab-org/gitlab/-/blob/8-13-stable-ee/doc/api/services.md#jira).
+
+```plaintext
+PUT /projects/:id/integrations/jira
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `url` | string | yes | The URL to the Jira project which is being linked to this GitLab project. For example, `https://jira.example.com`. |
+| `api_url` | string | no | The base URL to the Jira instance API. Web URL value is used if not set. For example, `https://jira-api.example.com`. |
+| `username` | string | yes | The username of the user created to be used with GitLab/Jira. |
+| `password` | string | yes | The password of the user created to be used with GitLab/Jira. |
+| `active` | boolean | no | Activates or deactivates the integration. Defaults to false (deactivated). |
+| `jira_issue_transition_automatic` | boolean | no | Enable [automatic issue transitions](../integration/jira/issues.md#automatic-issue-transitions). Takes precedence over `jira_issue_transition_id` if enabled. Defaults to `false` |
+| `jira_issue_transition_id` | string | no | The ID of one or more transitions for [custom issue transitions](../integration/jira/issues.md#custom-issue-transitions). Ignored if `jira_issue_transition_automatic` is enabled. Defaults to a blank string, which disables custom transitions. |
+| `commit_events` | boolean | false | Enable notifications for commit events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) |
+
+### Delete Jira integration
+
+Remove all previously Jira integrations from a project.
+
+```plaintext
+DELETE /projects/:id/integrations/jira
+```
+
+## Slack Slash Commands
+
+Ability to receive slash commands from a Slack chat instance.
+
+### Get Slack Slash Command integration settings
+
+Get Slack Slash Command integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/slack-slash-commands
+```
+
+Example response:
+
+```json
+{
+ "id": 4,
+ "title": "Slack slash commands",
+ "slug": "slack-slash-commands",
+ "created_at": "2017-06-27T05:51:39-07:00",
+ "updated_at": "2017-06-27T05:51:39-07:00",
+ "active": true,
+ "push_events": true,
+ "issues_events": true,
+ "confidential_issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "job_events": true,
+ "pipeline_events": true,
+ "comment_on_event_enabled": false,
+ "properties": {
+ "token": "<your_access_token>"
+ }
+}
+```
+
+### Create/Edit Slack Slash Commands integration
+
+Set Slack Slash Command for a project.
+
+```plaintext
+PUT /projects/:id/integrations/slack-slash-commands
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | yes | The Slack token |
+
+### Delete Slack Slash Command integration
+
+Delete Slack Slash Command integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/slack-slash-commands
+```
+
+## Mattermost Slash Commands
+
+Ability to receive slash commands from a Mattermost chat instance.
+
+### Get Mattermost Slash Command integration settings
+
+Get Mattermost Slash Command integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/mattermost-slash-commands
+```
+
+### Create/Edit Mattermost Slash Command integration
+
+Set Mattermost Slash Command for a project.
+
+```plaintext
+PUT /projects/:id/integrations/mattermost-slash-commands
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | yes | The Mattermost token |
+| `username` | string | no | The username to use to post the message |
+
+### Delete Mattermost Slash Command integration
+
+Delete Mattermost Slash Command integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/mattermost-slash-commands
+```
+
+## Packagist
+
+Update your project on Packagist (the main Composer repository) when commits or tags are pushed to GitLab.
+
+### Create/Edit Packagist integration
+
+Set Packagist integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/packagist
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `username` | string | yes | The username of a Packagist account |
+| `token` | string | yes | API token to the Packagist server |
+| `server` | boolean | no | URL of the Packagist server. Leave blank for default: <https://packagist.org> |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+
+### Delete Packagist integration
+
+Delete Packagist integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/packagist
+```
+
+### Get Packagist integration settings
+
+Get Packagist integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/packagist
+```
+
+## Pipeline-Emails
+
+Get emails for GitLab CI/CD pipelines.
+
+### Create/Edit Pipeline-Emails integration
+
+Set Pipeline-Emails integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/pipelines-email
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
+| `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" |
+| `notify_only_default_branch` | boolean | no | Send notifications only for the default branch ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28271)) |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+
+### Delete Pipeline-Emails integration
+
+Delete Pipeline-Emails integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/pipelines-email
+```
+
+### Get Pipeline-Emails integration settings
+
+Get Pipeline-Emails integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/pipelines-email
+```
+
+## Pivotal Tracker
+
+Add commit messages as comments to Pivotal Tracker stories.
+
+See also the [Pivotal Tracker integration documentation](../user/project/integrations/pivotal_tracker.md).
+
+### Create/Edit Pivotal Tracker integration
+
+Set Pivotal Tracker integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/pivotaltracker
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | The Pivotal Tracker token |
+| `restrict_to_branch` | boolean | false | Comma-separated list of branches to automatically inspect. Leave blank to include all branches. |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Pivotal Tracker integration
+
+Delete Pivotal Tracker integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/pivotaltracker
+```
+
+### Get Pivotal Tracker integration settings
+
+Get Pivotal Tracker integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/pivotaltracker
+```
+
+## Prometheus
+
+Prometheus is a powerful time-series monitoring service.
+
+### Create/Edit Prometheus integration
+
+Set Prometheus integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/prometheus
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_url` | string | true | Prometheus API Base URL. For example, `http://prometheus.example.com/`. |
+| `google_iap_audience_client_id` | string | false | Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com) |
+| `google_iap_service_account_json` | string | false | `credentials.json` file for your service account, like { "type": "service_account", "project_id": ... } |
+
+### Delete Prometheus integration
+
+Delete Prometheus integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/prometheus
+```
+
+### Get Prometheus integration settings
+
+Get Prometheus integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/prometheus
+```
+
+## Pushover
+
+Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.
+
+### Create/Edit Pushover integration
+
+Set Pushover integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/pushover
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `api_key` | string | true | Your application key |
+| `user_key` | string | true | Your user key |
+| `priority` | string | true | The priority |
+| `device` | string | false | Leave blank for all active devices |
+| `sound` | string | false | The sound of the notification |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Pushover integration
+
+Delete Pushover integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/pushover
+```
+
+### Get Pushover integration settings
+
+Get Pushover integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/pushover
+```
+
+## Redmine
+
+Redmine issue tracker
+
+### Create/Edit Redmine integration
+
+Set Redmine integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/redmine
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `new_issue_url` | string | true | New Issue URL |
+| `project_url` | string | true | Project URL |
+| `issues_url` | string | true | Issue URL |
+| `description` | string | false | Description |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete Redmine integration
+
+Delete Redmine integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/redmine
+```
+
+### Get Redmine integration settings
+
+Get Redmine integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/redmine
+```
+
+## Slack notifications
+
+Receive event notifications in Slack
+
+### Create/Edit Slack integration
+
+Set Slack integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/slack
+```
+
+NOTE:
+Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | `https://hooks.slack.com/services/...` |
+| `username` | string | false | username |
+| `channel` | string | false | Default channel to use if others are not configured |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
+| `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" |
+| `commit_events` | boolean | false | Enable notifications for commit events |
+| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `confidential_note_channel` | string | false | The name of the channel to receive confidential note events notifications |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `deployment_channel` | string | false | The name of the channel to receive deployment events notifications |
+| `deployment_events` | boolean | false | Enable notifications for deployment events |
+| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `job_events` | boolean | false | Enable notifications for job events |
+| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `note_channel` | string | false | The name of the channel to receive note events notifications |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `push_channel` | string | false | The name of the channel to receive push events notifications |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Slack integration
+
+Delete Slack integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/slack
+```
+
+### Get Slack integration settings
+
+Get Slack integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/slack
+```
+
+## Microsoft Teams
+
+Group Chat Software
+
+### Create/Edit Microsoft Teams integration
+
+Set Microsoft Teams integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/microsoft-teams
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Microsoft Teams webhook. For example, `https://outlook.office.com/webhook/...` |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
+| `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" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Microsoft Teams integration
+
+Delete Microsoft Teams integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/microsoft-teams
+```
+
+### Get Microsoft Teams integration settings
+
+Get Microsoft Teams integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/microsoft-teams
+```
+
+## Mattermost notifications
+
+Receive event notifications in Mattermost
+
+### Create/Edit Mattermost notifications integration
+
+Set Mattermost notifications integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/mattermost
+```
+
+NOTE:
+Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Mattermost webhook. For example, `http://mattermost_host/hooks/...` |
+| `username` | string | false | username |
+| `channel` | string | false | Default channel to use if others are not configured |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
+| `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" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+| `push_channel` | string | false | The name of the channel to receive push events notifications |
+| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
+| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
+| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
+| `note_channel` | string | false | The name of the channel to receive note events notifications |
+| `confidential_note_channel` | string | false | The name of the channel to receive confidential note events notifications |
+| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
+| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
+| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
+
+### Delete Mattermost notifications integration
+
+Delete Mattermost notifications integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/mattermost
+```
+
+### Get Mattermost notifications integration settings
+
+Get Mattermost notifications integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/mattermost
+```
+
+## JetBrains TeamCity CI
+
+A continuous integration and build server
+
+### Create/Edit JetBrains TeamCity CI integration
+
+Set JetBrains TeamCity CI integration for a project.
+
+> The build configuration in TeamCity must use the build format number `%build.vcs.number%`. Configure monitoring of all branches so merge requests build. That setting is in the VSC root advanced settings.
+
+```plaintext
+PUT /projects/:id/integrations/teamcity
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `teamcity_url` | string | true | TeamCity root URL. For example, `https://teamcity.example.com` |
+| `build_type` | string | true | Build configuration ID |
+| `username` | string | true | A user with permissions to trigger a manual build |
+| `password` | string | true | The password of the user |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete JetBrains TeamCity CI integration
+
+Delete JetBrains TeamCity CI integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/teamcity
+```
+
+### Get JetBrains TeamCity CI integration settings
+
+Get JetBrains TeamCity CI integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/teamcity
+```
+
+## Jenkins CI
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI integration
+
+Set Jenkins CI integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/jenkins
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `jenkins_url` | string | true | Jenkins URL like `http://jenkins.example.com`. |
+| `project_name` | string | true | The URL-friendly project name. Example: `my_project_name`. |
+| `username` | string | false | Username for authentication with the Jenkins server, if authentication is required by the server. |
+| `password` | string | false | Password for authentication with the Jenkins server, if authentication is required by the server. |
+| `push_events` | boolean | false | Enable notifications for push events. |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events. |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events. |
+
+### Delete Jenkins CI integration
+
+Delete Jenkins CI integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/jenkins
+```
+
+### Get Jenkins CI integration settings
+
+Get Jenkins CI integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/jenkins
+```
+
+## Jenkins CI (Deprecated) integration
+
+A continuous integration and build server
+
+NOTE:
+This integration was [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/1600) in GitLab 13.0.
+
+### Create/Edit Jenkins CI (Deprecated) integration
+
+Set Jenkins CI (Deprecated) integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/jenkins-deprecated
+```
+
+Parameters:
+
+- `project_url` (**required**) - Jenkins project URL like `http://jenkins.example.com/job/my-project/`
+- `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin
+- `pass_unstable` (optional) - Unstable builds are treated as passing
+
+### Delete Jenkins CI (Deprecated) integration
+
+Delete Jenkins CI (Deprecated) integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/jenkins-deprecated
+```
+
+### Get Jenkins CI (Deprecated) integration settings
+
+Get Jenkins CI (Deprecated) integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/jenkins-deprecated
+```
+
+## MockCI
+
+Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock integration.
+
+This integration is only available when your environment is set to development.
+
+### Create/Edit MockCI integration
+
+Set MockCI integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/mock-ci
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `mock_service_url` | string | true | `http://localhost:4004` |
+
+### Delete MockCI integration
+
+Delete MockCI integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/mock-ci
+```
+
+### Get MockCI integration settings
+
+Get MockCI integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/mock-ci
+```
+
+## YouTrack
+
+YouTrack issue tracker
+
+### Create/Edit YouTrack integration
+
+Set YouTrack integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/youtrack
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `issues_url` | string | true | Issue URL |
+| `project_url` | string | true | Project URL |
+| `description` | string | false | Description |
+| `push_events` | boolean | false | Enable notifications for push events |
+
+### Delete YouTrack integration
+
+Delete YouTrack integration for a project.
+
+```plaintext
+DELETE /projects/:id/integrations/youtrack
+```
+
+### Get YouTrack integration settings
+
+Get YouTrack integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/youtrack
+```
diff --git a/doc/api/project_repository_storage_moves.md b/doc/api/project_repository_storage_moves.md
index ebb15e1c1d6..b779a0046c6 100644
--- a/doc/api/project_repository_storage_moves.md
+++ b/doc/api/project_repository_storage_moves.md
@@ -10,7 +10,7 @@ type: reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31285) in GitLab 13.0.
Project repositories including wiki and design repositories can be moved between storages. This can be useful when
-[migrating to Gitaly Cluster](../administration/gitaly/index.md#migrate-to-gitaly-cluster),
+[migrating to Gitaly Cluster](../administration/gitaly/index.md#migrating-to-gitaly-cluster),
for example.
As project repository storage moves are processed, they transition through different states. Values
diff --git a/doc/api/services.md b/doc/api/services.md
index cf6c5ec511b..7587e53c9db 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1,1471 +1,9 @@
---
-stage: Ecosystem
-group: Integrations
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+redirect_to: 'integrations.md'
+remove_date: '2021-11-09'
---
-# Services API **(FREE)**
+This file was moved to [another location](integrations.md).
-NOTE:
-This API requires an access token with the [Maintainer or Owner role](../user/permissions.md).
-
-## List all active services
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21330) in GitLab 12.7.
-
-Get a list of all active project services.
-
-```plaintext
-GET /projects/:id/services
-```
-
-Example response:
-
-```json
-[
- {
- "id": 75,
- "title": "Jenkins CI",
- "slug": "jenkins",
- "created_at": "2019-11-20T11:20:25.297Z",
- "updated_at": "2019-11-20T12:24:37.498Z",
- "active": true,
- "commit_events": true,
- "push_events": true,
- "issues_events": true,
- "confidential_issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": false,
- "note_events": true,
- "confidential_note_events": true,
- "pipeline_events": true,
- "wiki_page_events": true,
- "job_events": true,
- "comment_on_event_enabled": true
- },
- {
- "id": 76,
- "title": "Alerts endpoint",
- "slug": "alerts",
- "created_at": "2019-11-20T11:20:25.297Z",
- "updated_at": "2019-11-20T12:24:37.498Z",
- "active": true,
- "commit_events": true,
- "push_events": true,
- "issues_events": true,
- "confidential_issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "confidential_note_events": true,
- "pipeline_events": true,
- "wiki_page_events": true,
- "job_events": true,
- "comment_on_event_enabled": true
- }
-]
-```
-
-## Asana
-
-Add commit messages as comments to Asana tasks.
-
-See also the [Asana service documentation](../user/project/integrations/asana.md).
-
-### Create/Edit Asana service
-
-Set Asana service for a project.
-
-```plaintext
-PUT /projects/:id/services/asana
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `api_key` | string | true | User API token. User must have access to task. All comments are attributed to this user. |
-| `restrict_to_branch` | string | false | Comma-separated list of branches to be are automatically inspected. Leave blank to include all branches. |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Asana service
-
-Delete Asana service for a project.
-
-```plaintext
-DELETE /projects/:id/services/asana
-```
-
-### Get Asana service settings
-
-Get Asana service settings for a project.
-
-```plaintext
-GET /projects/:id/services/asana
-```
-
-## Assembla
-
-Project Management Software (Source Commits Endpoint)
-
-### Create/Edit Assembla service
-
-Set Assembla service for a project.
-
-```plaintext
-PUT /projects/:id/services/assembla
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | true | The authentication token
-| `subdomain` | string | false | The subdomain setting |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Assembla service
-
-Delete Assembla service for a project.
-
-```plaintext
-DELETE /projects/:id/services/assembla
-```
-
-### Get Assembla service settings
-
-Get Assembla service settings for a project.
-
-```plaintext
-GET /projects/:id/services/assembla
-```
-
-## Atlassian Bamboo CI
-
-A continuous integration and build server
-
-### Create/Edit Atlassian Bamboo CI service
-
-Set Atlassian Bamboo CI service for a project.
-
-> You must set up automatic revision labeling and a repository trigger in Bamboo.
-
-```plaintext
-PUT /projects/:id/services/bamboo
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `bamboo_url` | string | true | Bamboo root URL. For example, `https://bamboo.example.com`. |
-| `build_key` | string | true | Bamboo build plan key like KEY |
-| `username` | string | true | A user with API access, if applicable |
-| `password` | string | true | Password of the user |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Atlassian Bamboo CI service
-
-Delete Atlassian Bamboo CI service for a project.
-
-```plaintext
-DELETE /projects/:id/services/bamboo
-```
-
-### Get Atlassian Bamboo CI service settings
-
-Get Atlassian Bamboo CI service settings for a project.
-
-```plaintext
-GET /projects/:id/services/bamboo
-```
-
-## Bugzilla
-
-Bugzilla Issue Tracker
-
-### Create/Edit Bugzilla service
-
-Set Bugzilla service for a project.
-
-```plaintext
-PUT /projects/:id/services/bugzilla
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `new_issue_url` | string | true | New Issue URL |
-| `issues_url` | string | true | Issue URL |
-| `project_url` | string | true | Project URL |
-| `description` | string | false | Description |
-| `title` | string | false | Title |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Bugzilla Service
-
-Delete Bugzilla service for a project.
-
-```plaintext
-DELETE /projects/:id/services/bugzilla
-```
-
-### Get Bugzilla Service Settings
-
-Get Bugzilla service settings for a project.
-
-```plaintext
-GET /projects/:id/services/bugzilla
-```
-
-## Buildkite
-
-Continuous integration and deployments
-
-### Create/Edit Buildkite service
-
-Set Buildkite service for a project.
-
-```plaintext
-PUT /projects/:id/services/buildkite
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | true | Buildkite project GitLab token |
-| `project_url` | string | true | Pipeline URL. For example, `https://buildkite.com/example/pipeline` |
-| `enable_ssl_verification` | boolean | false | DEPRECATED: This parameter has no effect since SSL verification is always enabled |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Buildkite service
-
-Delete Buildkite service for a project.
-
-```plaintext
-DELETE /projects/:id/services/buildkite
-```
-
-### Get Buildkite service settings
-
-Get Buildkite service settings for a project.
-
-```plaintext
-GET /projects/:id/services/buildkite
-```
-
-## Campfire
-
-Send notifications about push events to Campfire chat rooms.
-[New users can no longer sign up for Campfire](https://basecamp.com/retired/campfire).
-
-### Create/Edit Campfire service
-
-Set Campfire service for a project.
-
-```plaintext
-PUT /projects/:id/services/campfire
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-|---------------|---------|----------|---------------------------------------------------------------------------------------------|
-| `token` | string | true | Campfire API token. To find it, log into Campfire and select **My info**. |
-| `subdomain` | string | false | Campfire subdomain. Text between `https://` and `.campfirenow.com` when you're logged in. |
-| `room` | string | false | Campfire room. The last part of the URL when you're in a room. |
-| `push_events` | boolean | false | Enable notifications for push events. |
-
-### Delete Campfire service
-
-Delete Campfire service for a project.
-
-```plaintext
-DELETE /projects/:id/services/campfire
-```
-
-### Get Campfire service settings
-
-Get Campfire service settings for a project.
-
-```plaintext
-GET /projects/:id/services/campfire
-```
-
-## Unify Circuit
-
-Unify Circuit RTC and collaboration tool.
-
-### Create/Edit Unify Circuit service
-
-Set Unify Circuit service for a project.
-
-```plaintext
-PUT /projects/:id/services/unify-circuit
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Unify Circuit webhook. For example, `https://circuit.com/rest/v2/webhooks/incoming/...`. |
-| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
-| `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" |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `issues_events` | boolean | false | Enable notifications for issue events |
-| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `note_events` | boolean | false | Enable notifications for note events |
-| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
-
-### Delete Unify Circuit service
-
-Delete Unify Circuit service for a project.
-
-```plaintext
-DELETE /projects/:id/services/unify-circuit
-```
-
-### Get Unify Circuit service settings
-
-Get Unify Circuit service settings for a project.
-
-```plaintext
-GET /projects/:id/services/unify-circuit
-```
-
-## Webex Teams
-
-Webex Teams collaboration tool.
-
-### Create/Edit Webex Teams service
-
-Set Webex Teams service for a project.
-
-```plaintext
-PUT /projects/:id/services/webex-teams
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Webex Teams webhook. For example, `https://api.ciscospark.com/v1/webhooks/incoming/...`. |
-| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
-| `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" |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `issues_events` | boolean | false | Enable notifications for issue events |
-| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `note_events` | boolean | false | Enable notifications for note events |
-| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
-
-### Delete Webex Teams service
-
-Delete Webex Teams service for a project.
-
-```plaintext
-DELETE /projects/:id/services/webex-teams
-```
-
-### Get Webex Teams service settings
-
-Get Webex Teams service settings for a project.
-
-```plaintext
-GET /projects/:id/services/webex-teams
-```
-
-## Custom Issue Tracker
-
-Custom issue tracker
-
-### Create/Edit Custom Issue Tracker service
-
-Set Custom Issue Tracker service for a project.
-
-```plaintext
-PUT /projects/:id/services/custom-issue-tracker
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `new_issue_url` | string | true | New Issue URL |
-| `issues_url` | string | true | Issue URL |
-| `project_url` | string | true | Project URL |
-| `description` | string | false | Description |
-| `title` | string | false | Title |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Custom Issue Tracker service
-
-Delete Custom Issue Tracker service for a project.
-
-```plaintext
-DELETE /projects/:id/services/custom-issue-tracker
-```
-
-### Get Custom Issue Tracker service settings
-
-Get Custom Issue Tracker service settings for a project.
-
-```plaintext
-GET /projects/:id/services/custom-issue-tracker
-```
-
-## Drone CI
-
-Drone is a Continuous Integration platform built on Docker, written in Go
-
-### Create/Edit Drone CI service
-
-Set Drone CI service for a project.
-
-```plaintext
-PUT /projects/:id/services/drone-ci
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | true | Drone CI project specific token |
-| `drone_url` | string | true | `http://drone.example.com` |
-| `enable_ssl_verification` | boolean | false | Enable SSL verification |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-
-### Delete Drone CI service
-
-Delete Drone CI service for a project.
-
-```plaintext
-DELETE /projects/:id/services/drone-ci
-```
-
-### Get Drone CI service settings
-
-Get Drone CI service settings for a project.
-
-```plaintext
-GET /projects/:id/services/drone-ci
-```
-
-## Emails on push
-
-Email the commits and diff of each push to a list of recipients.
-
-### Create/Edit Emails on push service
-
-Set Emails on push service for a project.
-
-```plaintext
-PUT /projects/:id/services/emails-on-push
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | true | Emails separated by whitespace |
-| `disable_diffs` | boolean | false | Disable code diffs |
-| `send_from_committer_email` | boolean | false | Send from committer |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected". Notifications are always fired for tag pushes. The default value is "all" |
-
-### Delete Emails on push service
-
-Delete Emails on push service for a project.
-
-```plaintext
-DELETE /projects/:id/services/emails-on-push
-```
-
-### Get Emails on push service settings
-
-Get Emails on push service settings for a project.
-
-```plaintext
-GET /projects/:id/services/emails-on-push
-```
-
-## Confluence service
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220934) in GitLab 13.2.
-
-Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace.
-
-### Create/Edit Confluence service
-
-Set Confluence service for a project.
-
-```plaintext
-PUT /projects/:id/services/confluence
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `confluence_url` | string | true | The URL of the Confluence Cloud Workspace hosted on atlassian.net. |
-
-### Delete Confluence service
-
-Delete Confluence service for a project.
-
-```plaintext
-DELETE /projects/:id/services/confluence
-```
-
-### Get Confluence service settings
-
-Get Confluence service settings for a project.
-
-```plaintext
-GET /projects/:id/services/confluence
-```
-
-## External wiki
-
-Replaces the link to the internal wiki with a link to an external wiki.
-
-### Create/Edit External wiki service
-
-Set External wiki service for a project.
-
-```plaintext
-PUT /projects/:id/services/external-wiki
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `external_wiki_url` | string | true | The URL of the external wiki |
-
-### Delete External wiki service
-
-Delete External wiki service for a project.
-
-```plaintext
-DELETE /projects/:id/services/external-wiki
-```
-
-### Get External wiki service settings
-
-Get External wiki service settings for a project.
-
-```plaintext
-GET /projects/:id/services/external-wiki
-```
-
-## Flowdock
-
-Flowdock is a ChatOps application for collaboration in software engineering
-companies. You can send notifications from GitLab events to Flowdock flows.
-For integration instructions, see the [Flowdock documentation](https://www.flowdock.com/help/gitlab).
-
-### Create/Edit Flowdock service
-
-Set Flowdock service for a project.
-
-```plaintext
-PUT /projects/:id/services/flowdock
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | true | Flowdock Git source token |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Flowdock service
-
-Delete Flowdock service for a project.
-
-```plaintext
-DELETE /projects/:id/services/flowdock
-```
-
-### Get Flowdock service settings
-
-Get Flowdock service settings for a project.
-
-```plaintext
-GET /projects/:id/services/flowdock
-```
-
-## GitHub **(PREMIUM)**
-
-Code collaboration software.
-
-### Create/Edit GitHub service
-
-Set GitHub service for a project.
-
-```plaintext
-PUT /projects/:id/services/github
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | true | GitHub API token with `repo:status` OAuth scope |
-| `repository_url` | string | true | GitHub repository URL |
-| `static_context` | boolean | false | Append instance name instead of branch to [status check name](../user/project/integrations/github.md#static--dynamic-status-check-names) |
-
-### Delete GitHub service
-
-Delete GitHub service for a project.
-
-```plaintext
-DELETE /projects/:id/services/github
-```
-
-### Get GitHub service settings
-
-Get GitHub service settings for a project.
-
-```plaintext
-GET /projects/:id/services/github
-```
-
-## Hangouts Chat
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20290) in GitLab 11.2.
-
-Google Workspace team collaboration tool.
-
-### Create/Edit Hangouts Chat service
-
-Set Hangouts Chat service for a project.
-
-```plaintext
-PUT /projects/:id/services/hangouts-chat
-```
-
-NOTE:
-Specific event parameters (for example, `push_events` flag) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Hangouts Chat webhook. For example, `https://chat.googleapis.com/v1/spaces...`. |
-| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
-| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
-| `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" |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `issues_events` | boolean | false | Enable notifications for issue events |
-| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `note_events` | boolean | false | Enable notifications for note events |
-| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
-
-### Delete Hangouts Chat service
-
-Delete Hangouts Chat service for a project.
-
-```plaintext
-DELETE /projects/:id/services/hangouts-chat
-```
-
-### Get Hangouts Chat service settings
-
-Get Hangouts Chat service settings for a project.
-
-```plaintext
-GET /projects/:id/services/hangouts-chat
-```
-
-## irker (IRC gateway)
-
-Send IRC messages, on update, to a list of recipients through an irker gateway.
-
-For more information, see the [irker integration documentation](../user/project/integrations/irker.md).
-
-### Create/Edit irker (IRC gateway) service
-
-Set irker (IRC gateway) service for a project.
-
-```plaintext
-PUT /projects/:id/services/irker
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | true | Recipients/channels separated by whitespaces |
-| `default_irc_uri` | string | false | `irc://irc.network.net:6697/` |
-| `server_host` | string | false | localhost |
-| `server_port` | integer | false | 6659 |
-| `colorize_messages` | boolean | false | Colorize messages |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete irker (IRC gateway) service
-
-Delete irker (IRC gateway) service for a project.
-
-```plaintext
-DELETE /projects/:id/services/irker
-```
-
-### Get irker (IRC gateway) service settings
-
-Get irker (IRC gateway) service settings for a project.
-
-```plaintext
-GET /projects/:id/services/irker
-```
-
-## Jira
-
-Jira issue tracker.
-
-### Get Jira service settings
-
-Get Jira service settings for a project.
-
-```plaintext
-GET /projects/:id/services/jira
-```
-
-### Create/Edit Jira service
-
-Set Jira service for a project.
-
-> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and
-> `project_url` are replaced by `url`. If you are using an
-> older version, [follow this documentation](https://gitlab.com/gitlab-org/gitlab/-/blob/8-13-stable-ee/doc/api/services.md#jira).
-
-```plaintext
-PUT /projects/:id/services/jira
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `url` | string | yes | The URL to the Jira project which is being linked to this GitLab project. For example, `https://jira.example.com`. |
-| `api_url` | string | no | The base URL to the Jira instance API. Web URL value is used if not set. For example, `https://jira-api.example.com`. |
-| `username` | string | yes | The username of the user created to be used with GitLab/Jira. |
-| `password` | string | yes | The password of the user created to be used with GitLab/Jira. |
-| `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). |
-| `jira_issue_transition_automatic` | boolean | no | Enable [automatic issue transitions](../integration/jira/issues.md#automatic-issue-transitions). Takes precedence over `jira_issue_transition_id` if enabled. Defaults to `false` |
-| `jira_issue_transition_id` | string | no | The ID of one or more transitions for [custom issue transitions](../integration/jira/issues.md#custom-issue-transitions). Ignored if `jira_issue_transition_automatic` is enabled. Defaults to a blank string, which disables custom transitions. |
-| `commit_events` | boolean | false | Enable notifications for commit events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) |
-
-### Delete Jira service
-
-Remove all previously Jira settings from a project.
-
-```plaintext
-DELETE /projects/:id/services/jira
-```
-
-## Slack slash commands
-
-Ability to receive slash commands from a Slack chat instance.
-
-### Get Slack slash command service settings
-
-Get Slack slash command service settings for a project.
-
-```plaintext
-GET /projects/:id/services/slack-slash-commands
-```
-
-Example response:
-
-```json
-{
- "id": 4,
- "title": "Slack slash commands",
- "slug": "slack-slash-commands",
- "created_at": "2017-06-27T05:51:39-07:00",
- "updated_at": "2017-06-27T05:51:39-07:00",
- "active": true,
- "push_events": true,
- "issues_events": true,
- "confidential_issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "pipeline_events": true,
- "comment_on_event_enabled": false,
- "properties": {
- "token": "<your_access_token>"
- }
-}
-```
-
-### Create/Edit Slack slash command service
-
-Set Slack slash command for a project.
-
-```plaintext
-PUT /projects/:id/services/slack-slash-commands
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | yes | The Slack token |
-
-### Delete Slack slash command service
-
-Delete Slack slash command service for a project.
-
-```plaintext
-DELETE /projects/:id/services/slack-slash-commands
-```
-
-## Mattermost slash commands
-
-Ability to receive slash commands from a Mattermost chat instance.
-
-### Get Mattermost slash command service settings
-
-Get Mattermost slash command service settings for a project.
-
-```plaintext
-GET /projects/:id/services/mattermost-slash-commands
-```
-
-### Create/Edit Mattermost slash command service
-
-Set Mattermost slash command for a project.
-
-```plaintext
-PUT /projects/:id/services/mattermost-slash-commands
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | yes | The Mattermost token |
-| `username` | string | no | The username to use to post the message |
-
-### Delete Mattermost slash command service
-
-Delete Mattermost slash command service for a project.
-
-```plaintext
-DELETE /projects/:id/services/mattermost-slash-commands
-```
-
-## Packagist
-
-Update your project on Packagist (the main Composer repository) when commits or tags are pushed to GitLab.
-
-### Create/Edit Packagist service
-
-Set Packagist service for a project.
-
-```plaintext
-PUT /projects/:id/services/packagist
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `username` | string | yes | The username of a Packagist account |
-| `token` | string | yes | API token to the Packagist server |
-| `server` | boolean | no | URL of the Packagist server. Leave blank for default: <https://packagist.org> |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-
-### Delete Packagist service
-
-Delete Packagist service for a project.
-
-```plaintext
-DELETE /projects/:id/services/packagist
-```
-
-### Get Packagist service settings
-
-Get Packagist service settings for a project.
-
-```plaintext
-GET /projects/:id/services/packagist
-```
-
-## Pipeline-Emails
-
-Get emails for GitLab CI/CD pipelines.
-
-### Create/Edit Pipeline-Emails service
-
-Set Pipeline-Emails service for a project.
-
-```plaintext
-PUT /projects/:id/services/pipelines-email
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | yes | Comma-separated list of recipient email addresses |
-| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
-| `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" |
-| `notify_only_default_branch` | boolean | no | Send notifications only for the default branch ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28271)) |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-
-### Delete Pipeline-Emails service
-
-Delete Pipeline-Emails service for a project.
-
-```plaintext
-DELETE /projects/:id/services/pipelines-email
-```
-
-### Get Pipeline-Emails service settings
-
-Get Pipeline-Emails service settings for a project.
-
-```plaintext
-GET /projects/:id/services/pipelines-email
-```
-
-## Pivotal Tracker
-
-Add commit messages as comments to Pivotal Tracker stories.
-
-See also the [Pivotal Tracker service documentation](../user/project/integrations/pivotal_tracker.md).
-
-### Create/Edit Pivotal Tracker service
-
-Set Pivotal Tracker service for a project.
-
-```plaintext
-PUT /projects/:id/services/pivotaltracker
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `token` | string | true | The Pivotal Tracker token |
-| `restrict_to_branch` | boolean | false | Comma-separated list of branches to automatically inspect. Leave blank to include all branches. |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Pivotal Tracker service
-
-Delete Pivotal Tracker service for a project.
-
-```plaintext
-DELETE /projects/:id/services/pivotaltracker
-```
-
-### Get Pivotal Tracker service settings
-
-Get Pivotal Tracker service settings for a project.
-
-```plaintext
-GET /projects/:id/services/pivotaltracker
-```
-
-## Prometheus
-
-Prometheus is a powerful time-series monitoring service.
-
-### Create/Edit Prometheus service
-
-Set Prometheus service for a project.
-
-```plaintext
-PUT /projects/:id/services/prometheus
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `api_url` | string | true | Prometheus API Base URL. For example, `http://prometheus.example.com/`. |
-| `google_iap_audience_client_id` | string | false | Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com) |
-| `google_iap_service_account_json` | string | false | `credentials.json` file for your service account, like { "type": "service_account", "project_id": ... } |
-
-### Delete Prometheus service
-
-Delete Prometheus service for a project.
-
-```plaintext
-DELETE /projects/:id/services/prometheus
-```
-
-### Get Prometheus service settings
-
-Get Prometheus service settings for a project.
-
-```plaintext
-GET /projects/:id/services/prometheus
-```
-
-## Pushover
-
-Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.
-
-### Create/Edit Pushover service
-
-Set Pushover service for a project.
-
-```plaintext
-PUT /projects/:id/services/pushover
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `api_key` | string | true | Your application key |
-| `user_key` | string | true | Your user key |
-| `priority` | string | true | The priority |
-| `device` | string | false | Leave blank for all active devices |
-| `sound` | string | false | The sound of the notification |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Pushover service
-
-Delete Pushover service for a project.
-
-```plaintext
-DELETE /projects/:id/services/pushover
-```
-
-### Get Pushover service settings
-
-Get Pushover service settings for a project.
-
-```plaintext
-GET /projects/:id/services/pushover
-```
-
-## Redmine
-
-Redmine issue tracker
-
-### Create/Edit Redmine service
-
-Set Redmine service for a project.
-
-```plaintext
-PUT /projects/:id/services/redmine
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `new_issue_url` | string | true | New Issue URL |
-| `project_url` | string | true | Project URL |
-| `issues_url` | string | true | Issue URL |
-| `description` | string | false | Description |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete Redmine service
-
-Delete Redmine service for a project.
-
-```plaintext
-DELETE /projects/:id/services/redmine
-```
-
-### Get Redmine service settings
-
-Get Redmine service settings for a project.
-
-```plaintext
-GET /projects/:id/services/redmine
-```
-
-## Slack notifications
-
-Receive event notifications in Slack
-
-### Create/Edit Slack service
-
-Set Slack service for a project.
-
-```plaintext
-PUT /projects/:id/services/slack
-```
-
-NOTE:
-Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | `https://hooks.slack.com/services/...` |
-| `username` | string | false | username |
-| `channel` | string | false | Default channel to use if others are not configured |
-| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
-| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
-| `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" |
-| `commit_events` | boolean | false | Enable notifications for commit events |
-| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
-| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
-| `confidential_note_channel` | string | false | The name of the channel to receive confidential note events notifications |
-| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
-| `deployment_channel` | string | false | The name of the channel to receive deployment events notifications |
-| `deployment_events` | boolean | false | Enable notifications for deployment events |
-| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
-| `issues_events` | boolean | false | Enable notifications for issue events |
-| `job_events` | boolean | false | Enable notifications for job events |
-| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `note_channel` | string | false | The name of the channel to receive note events notifications |
-| `note_events` | boolean | false | Enable notifications for note events |
-| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-| `push_channel` | string | false | The name of the channel to receive push events notifications |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
-| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
-| `vulnerability_channel` | string | false | **(ULTIMATE)** The name of the channel to receive vulnerability event notifications. |
-| `vulnerability_events` | boolean | false | **(ULTIMATE)** Enable notifications for vulnerability events |
-
-### Delete Slack service
-
-Delete Slack service for a project.
-
-```plaintext
-DELETE /projects/:id/services/slack
-```
-
-### Get Slack service settings
-
-Get Slack service settings for a project.
-
-```plaintext
-GET /projects/:id/services/slack
-```
-
-## Microsoft Teams
-
-Group Chat Software
-
-### Create/Edit Microsoft Teams service
-
-Set Microsoft Teams service for a project.
-
-```plaintext
-PUT /projects/:id/services/microsoft-teams
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Microsoft Teams webhook. For example, `https://outlook.office.com/webhook/...` |
-| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
-| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
-| `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" |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `issues_events` | boolean | false | Enable notifications for issue events |
-| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `note_events` | boolean | false | Enable notifications for note events |
-| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
-
-### Delete Microsoft Teams service
-
-Delete Microsoft Teams service for a project.
-
-```plaintext
-DELETE /projects/:id/services/microsoft-teams
-```
-
-### Get Microsoft Teams service settings
-
-Get Microsoft Teams service settings for a project.
-
-```plaintext
-GET /projects/:id/services/microsoft-teams
-```
-
-## Mattermost notifications
-
-Receive event notifications in Mattermost
-
-### Create/Edit Mattermost notifications service
-
-Set Mattermost service for a project.
-
-```plaintext
-PUT /projects/:id/services/mattermost
-```
-
-NOTE:
-Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Mattermost webhook. For example, `http://mattermost_host/hooks/...` |
-| `username` | string | false | username |
-| `channel` | string | false | Default channel to use if others are not configured |
-| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
-| `notify_only_default_branch` | boolean | false | DEPRECATED: This parameter has been replaced with `branches_to_be_notified` |
-| `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" |
-| `push_events` | boolean | false | Enable notifications for push events |
-| `issues_events` | boolean | false | Enable notifications for issue events |
-| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events |
-| `note_events` | boolean | false | Enable notifications for note events |
-| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
-| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
-| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
-| `vulnerability_events` | boolean | false | **(ULTIMATE)** Enable notifications for vulnerability events |
-| `push_channel` | string | false | The name of the channel to receive push events notifications |
-| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
-| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
-| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
-| `note_channel` | string | false | The name of the channel to receive note events notifications |
-| `confidential_note_channel` | string | false | The name of the channel to receive confidential note events notifications |
-| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
-| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
-| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
-| `vulnerability_channel` | string | false | **(ULTIMATE)** The name of the channel to receive vulnerability events notifications |
-
-### Delete Mattermost notifications service
-
-Delete Mattermost Notifications service for a project.
-
-```plaintext
-DELETE /projects/:id/services/mattermost
-```
-
-### Get Mattermost notifications service settings
-
-Get Mattermost notifications service settings for a project.
-
-```plaintext
-GET /projects/:id/services/mattermost
-```
-
-## JetBrains TeamCity CI
-
-A continuous integration and build server
-
-### Create/Edit JetBrains TeamCity CI service
-
-Set JetBrains TeamCity CI service for a project.
-
-> The build configuration in TeamCity must use the build format number `%build.vcs.number%`. Configure monitoring of all branches so merge requests build. That setting is in the VSC root advanced settings.
-
-```plaintext
-PUT /projects/:id/services/teamcity
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `teamcity_url` | string | true | TeamCity root URL. For example, `https://teamcity.example.com` |
-| `build_type` | string | true | Build configuration ID |
-| `username` | string | true | A user with permissions to trigger a manual build |
-| `password` | string | true | The password of the user |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete JetBrains TeamCity CI service
-
-Delete JetBrains TeamCity CI service for a project.
-
-```plaintext
-DELETE /projects/:id/services/teamcity
-```
-
-### Get JetBrains TeamCity CI service settings
-
-Get JetBrains TeamCity CI service settings for a project.
-
-```plaintext
-GET /projects/:id/services/teamcity
-```
-
-## Jenkins CI
-
-A continuous integration and build server
-
-### Create/Edit Jenkins CI service
-
-Set Jenkins CI service for a project.
-
-```plaintext
-PUT /projects/:id/services/jenkins
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `jenkins_url` | string | true | Jenkins URL like `http://jenkins.example.com`. |
-| `project_name` | string | true | The URL-friendly project name. Example: `my_project_name`. |
-| `username` | string | false | Username for authentication with the Jenkins server, if authentication is required by the server. |
-| `password` | string | false | Password for authentication with the Jenkins server, if authentication is required by the server. |
-| `push_events` | boolean | false | Enable notifications for push events. |
-| `merge_requests_events` | boolean | false | Enable notifications for merge request events. |
-| `tag_push_events` | boolean | false | Enable notifications for tag push events. |
-
-### Delete Jenkins CI service
-
-Delete Jenkins CI service for a project.
-
-```plaintext
-DELETE /projects/:id/services/jenkins
-```
-
-### Get Jenkins CI service settings
-
-Get Jenkins CI service settings for a project.
-
-```plaintext
-GET /projects/:id/services/jenkins
-```
-
-## Jenkins CI (Deprecated) Service
-
-A continuous integration and build server
-
-NOTE:
-This service was [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/1600) in GitLab 13.0.
-
-### Create/Edit Jenkins CI (Deprecated) service
-
-Set Jenkins CI (Deprecated) service for a project.
-
-```plaintext
-PUT /projects/:id/services/jenkins-deprecated
-```
-
-Parameters:
-
-- `project_url` (**required**) - Jenkins project URL like `http://jenkins.example.com/job/my-project/`
-- `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin
-- `pass_unstable` (optional) - Unstable builds are treated as passing
-
-### Delete Jenkins CI (Deprecated) service
-
-Delete Jenkins CI (Deprecated) service for a project.
-
-```plaintext
-DELETE /projects/:id/services/jenkins-deprecated
-```
-
-### Get Jenkins CI (Deprecated) service settings
-
-Get Jenkins CI (Deprecated) service settings for a project.
-
-```plaintext
-GET /projects/:id/services/jenkins-deprecated
-```
-
-## MockCI
-
-Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
-
-This service is only available when your environment is set to development.
-
-### Create/Edit MockCI service
-
-Set MockCI service for a project.
-
-```plaintext
-PUT /projects/:id/services/mock-ci
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `mock_service_url` | string | true | `http://localhost:4004` |
-
-### Delete MockCI service
-
-Delete MockCI service for a project.
-
-```plaintext
-DELETE /projects/:id/services/mock-ci
-```
-
-### Get MockCI service settings
-
-Get MockCI service settings for a project.
-
-```plaintext
-GET /projects/:id/services/mock-ci
-```
-
-## YouTrack
-
-YouTrack issue tracker
-
-### Create/Edit YouTrack service
-
-Set YouTrack service for a project.
-
-```plaintext
-PUT /projects/:id/services/youtrack
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `issues_url` | string | true | Issue URL |
-| `project_url` | string | true | Project URL |
-| `description` | string | false | Description |
-| `push_events` | boolean | false | Enable notifications for push events |
-
-### Delete YouTrack Service
-
-Delete YouTrack service for a project.
-
-```plaintext
-DELETE /projects/:id/services/youtrack
-```
-
-### Get YouTrack Service Settings
-
-Get YouTrack service settings for a project.
-
-```plaintext
-GET /projects/:id/services/youtrack
-```
+<!-- This redirect file can be deleted after <2021-11-09>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/doc/api/snippet_repository_storage_moves.md b/doc/api/snippet_repository_storage_moves.md
index a73542c8505..81473fdff91 100644
--- a/doc/api/snippet_repository_storage_moves.md
+++ b/doc/api/snippet_repository_storage_moves.md
@@ -10,7 +10,7 @@ type: reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49228) in GitLab 13.8.
Snippet repositories can be moved between storages. This can be useful when
-[migrating to Gitaly Cluster](../administration/gitaly/index.md#migrate-to-gitaly-cluster), for
+[migrating to Gitaly Cluster](../administration/gitaly/index.md#migrating-to-gitaly-cluster), for
example.
As snippet repository storage moves are processed, they transition through different states. Values
diff --git a/doc/api/usage_data.md b/doc/api/usage_data.md
index f244c681086..4809166f357 100644
--- a/doc/api/usage_data.md
+++ b/doc/api/usage_data.md
@@ -13,7 +13,7 @@ The Service Data API is associated with [Service Ping](../development/service_pi
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57270) in GitLab 13.11.
-Export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](https://gitlab-org.gitlab.io/growth/product-intelligence/metric-dictionary), for easier importing.
+Export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](https://metrics.gitlab.com/index.html), for easier importing.
```plaintext
GET /usage_data/metric_definitions
diff --git a/doc/api/users.md b/doc/api/users.md
index 17864526f8a..d8effc4d38f 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -259,7 +259,7 @@ GET /users?with_custom_attributes=true
## Single user
-Get a single user. This endpoint can be accessed without authentication.
+Get a single user.
### For user
@@ -806,7 +806,7 @@ Example response:
### Followers and following
-Get the followers of a user. This endpoint can be accessed without authentication.
+Get the followers of a user.
```plaintext
GET /users/:id/followers
diff --git a/doc/development/fe_guide/storybook.md b/doc/development/fe_guide/storybook.md
index 3ccb13b5bff..d4b65f3232a 100644
--- a/doc/development/fe_guide/storybook.md
+++ b/doc/development/fe_guide/storybook.md
@@ -49,3 +49,9 @@ To add a story:
- Specify the `title` field of the story as the component's file path from the `javascripts/` directory,
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue`, specify the `title` as
`vue_shared/components/To-do Button`. This will ensure the Storybook navigation maps closely to our internal directory structure.
+
+## Mock backend APIs
+
+GitLab’s Storybook uses [MirajeJS](https://miragejs.com/) to mock REST and GraphQL APIs. Storybook shares the MirajeJS server
+with the [frontend integration tests](../testing_guide/testing_levels.md#frontend-integration-tests). You can find the MirajeJS
+configuration files in `spec/frontend_integration/mock_server`.
diff --git a/doc/development/redis.md b/doc/development/redis.md
index 13ec923c19a..a04ca94ad91 100644
--- a/doc/development/redis.md
+++ b/doc/development/redis.md
@@ -15,6 +15,8 @@ GitLab uses [Redis](https://redis.io) for the following distinct purposes:
- To manage the shared application state.
- To store CI trace chunks.
- As a Pub/Sub queue backend for ActionCable.
+- CI trace chunk storage.
+- Rate limiting state storage.
In most environments (including the GDK), all of these point to the same
Redis instance.
diff --git a/doc/development/service_ping/dictionary.md b/doc/development/service_ping/dictionary.md
index e7e8464ff7a..810c789bc03 100644
--- a/doc/development/service_ping/dictionary.md
+++ b/doc/development/service_ping/dictionary.md
@@ -1,4 +1,4 @@
---
-redirect_to: 'https://gitlab-org.gitlab.io/growth/product-intelligence/metric-dictionary'
+redirect_to: 'https://metrics.gitlab.com/index.html'
remove_date: '2021-11-10'
---
diff --git a/doc/development/service_ping/metrics_dictionary.md b/doc/development/service_ping/metrics_dictionary.md
index a2eb0ddb821..c1478e6290e 100644
--- a/doc/development/service_ping/metrics_dictionary.md
+++ b/doc/development/service_ping/metrics_dictionary.md
@@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Metrics Dictionary Guide
[Service Ping](index.md) metrics are defined in the
-[Metrics Dictionary](https://gitlab-org.gitlab.io/growth/product-intelligence/metric-dictionary).
+[Metrics Dictionary](https://metrics.gitlab.com/index.html).
This guide describes the dictionary and how it's implemented.
## Metrics Definition and validation
diff --git a/doc/development/service_ping/review_guidelines.md b/doc/development/service_ping/review_guidelines.md
index 048b705636f..eb64d460b5a 100644
--- a/doc/development/service_ping/review_guidelines.md
+++ b/doc/development/service_ping/review_guidelines.md
@@ -14,7 +14,7 @@ general best practices for code reviews, refer to our [code review guide](../cod
## Resources for reviewers
- [Service Ping Guide](index.md)
-- [Metrics Dictionary](https://gitlab-org.gitlab.io/growth/product-intelligence/metric-dictionary)
+- [Metrics Dictionary](https://metrics.gitlab.com/index.html)
## Review process
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index e7e8464ff7a..810c789bc03 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -1,4 +1,4 @@
---
-redirect_to: 'https://gitlab-org.gitlab.io/growth/product-intelligence/metric-dictionary'
+redirect_to: 'https://metrics.gitlab.com/index.html'
remove_date: '2021-11-10'
---
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 14e6f4dad3a..44537707db6 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -75,6 +75,7 @@ To enable 2FA:
1. **In GitLab:**
1. Enter the six-digit pin number from the entry on your device into the **Pin
code** field.
+ 1. Enter your current password.
1. Select **Submit**.
If the pin you entered was correct, a message displays indicating that
@@ -365,7 +366,8 @@ If you ever need to disable 2FA:
1. Sign in to your GitLab account.
1. Go to your [**User settings**](../index.md#access-your-user-settings).
1. Go to **Account**.
-1. Click **Disable**, under **Two-Factor Authentication**.
+1. Select **Manage two-factor authentication**.
+1. Under **Two-Factor Authentication**, enter your current password and select **Disable**.
This clears all your two-factor authentication registrations, including mobile
applications and U2F / WebAuthn devices.
@@ -460,7 +462,7 @@ To regenerate 2FA recovery codes, you need access to a desktop browser:
1. Go to your [**User settings**](../index.md#access-your-user-settings).
1. Select **Account > Two-Factor Authentication (2FA)**.
1. If you've already configured 2FA, click **Manage two-factor authentication**.
-1. In the **Register Two-Factor Authenticator** pane, click **Regenerate recovery codes**.
+1. In the **Register Two-Factor Authenticator** pane, enter your current password and select **Regenerate recovery codes**.
NOTE:
If you regenerate 2FA recovery codes, save them. You can't use any previously created 2FA codes.
diff --git a/doc/user/project/integrations/asana.md b/doc/user/project/integrations/asana.md
index e1e926da19b..963fca34827 100644
--- a/doc/user/project/integrations/asana.md
+++ b/doc/user/project/integrations/asana.md
@@ -4,9 +4,9 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Asana service **(FREE)**
+# Asana integration **(FREE)**
-This service adds commit messages as comments to Asana tasks.
+This integration adds commit messages as comments to Asana tasks.
Once enabled, commit messages are checked for Asana task URLs (for example,
`https://app.asana.com/0/123456/987654`) or task IDs starting with `#`
(for example, `#987654`). Every task ID found gets the commit comment added to it.
@@ -23,7 +23,7 @@ You can use either of these words:
- `closed`
- `closing`
-See also the [Asana service API documentation](../../../api/services.md#asana).
+See also the [Asana integration API documentation](../../../api/integrations.md#asana).
## Setup
diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md
index 249f6c4305c..cbe68bd1802 100644
--- a/doc/user/project/integrations/overview.md
+++ b/doc/user/project/integrations/overview.md
@@ -32,7 +32,7 @@ Click on the service links to see further configuration instructions and details
| [Bugzilla](bugzilla.md) | Use Bugzilla as the issue tracker. | **{dotted-circle}** No |
| Buildkite | Run CI/CD pipelines with Buildkite. | **{check-circle}** Yes |
| Campfire | Connect to chat. | **{dotted-circle}** No |
-| [Confluence Workspace](../../../api/services.md#confluence-service) | Replace the link to the internal wiki with a link to a Confluence Cloud Workspace. | **{dotted-circle}** No |
+| [Confluence Workspace](../../../api/integrations.md#confluence-integration) | Replace the link to the internal wiki with a link to a Confluence Cloud Workspace. | **{dotted-circle}** No |
| [Custom issue tracker](custom_issue_tracker.md) | Use a custom issue tracker. | **{dotted-circle}** No |
| [Datadog](../../../integration/datadog.md) | Trace your GitLab pipelines with Datadog. | **{check-circle}** Yes |
| [Discord Notifications](discord_notifications.md) | Send notifications about project events to a Discord channel. | **{dotted-circle}** No |
@@ -40,7 +40,7 @@ Click on the service links to see further configuration instructions and details
| [Emails on push](emails_on_push.md) | Send commits and diff of each push by email. | **{dotted-circle}** No |
| [EWM](ewm.md) | Use IBM Engineering Workflow Management as the issue tracker. | **{dotted-circle}** No |
| [External wiki](../wiki/index.md#link-an-external-wiki) | Link an external wiki. | **{dotted-circle}** No |
-| [Flowdock](../../../api/services.md#flowdock) | Send notifications from GitLab to Flowdock flows. | **{dotted-circle}** No |
+| [Flowdock](../../../api/integrations.md#flowdock) | Send notifications from GitLab to Flowdock flows. | **{dotted-circle}** No |
| [GitHub](github.md) | Obtain statuses for commits and pull requests. | **{dotted-circle}** No |
| [Google Chat](hangouts_chat.md) | Send notifications from your GitLab project to a room in Google Chat.| **{dotted-circle}** No |
| [irker (IRC gateway)](irker.md) | Send IRC messages. | **{dotted-circle}** No |
diff --git a/doc/user/project/integrations/pivotal_tracker.md b/doc/user/project/integrations/pivotal_tracker.md
index d464007dd5e..93a3490e4b6 100644
--- a/doc/user/project/integrations/pivotal_tracker.md
+++ b/doc/user/project/integrations/pivotal_tracker.md
@@ -29,7 +29,7 @@ Read more about the
[Source Commits endpoint](https://www.pivotaltracker.com/help/api/rest/v5#Source_Commits) in
the Pivotal Tracker API documentation.
-See also the [Pivotal Tracker service API documentation](../../../api/services.md#pivotal-tracker).
+See also the [Pivotal Tracker integration API documentation](../../../api/integrations.md#pivotal-tracker).
## Set up Pivotal Tracker
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 8d14e461e49..f23ac2c3349 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -135,9 +135,11 @@ The following items are **not** exported:
- Build traces and artifacts
- Container registry images
- CI/CD variables
+- Pipeline triggers
- Webhooks
- Any encrypted tokens
- Merge Request Approvers
+- Repository size limits
These content rules also apply to creating projects from templates on the
[group](../../group/custom_project_templates.md)
@@ -261,7 +263,7 @@ reduce the repository size for another import attempt.
git gc --prune=now --aggressive
# Prepare recreating an importable file
- git bundle create ../project.bundle smaller-tmp-main
+ git bundle create ../project.bundle <default-branch-name>
cd ..
mv project/ ../"$EXPORT"-project
cd ..
diff --git a/lib/api/api.rb b/lib/api/api.rb
index a68d3267740..a5340d9baaf 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -260,7 +260,7 @@ module API
mount ::API::ResourceAccessTokens
mount ::API::RubygemPackages
mount ::API::Search
- mount ::API::Services
+ mount ::API::Integrations
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::SnippetRepositoryStorageMoves
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index 06539772568..0aec038484a 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -2,7 +2,7 @@
module API
module Helpers
- # Helpers module for API::Services
+ # Helpers module for API::Integrations
#
# The data structures inside this model are returned using class methods,
# allowing EE to extend them where necessary.
diff --git a/lib/api/import_bitbucket_server.rb b/lib/api/import_bitbucket_server.rb
index ecd78c6e6db..0f0d62dcbfb 100644
--- a/lib/api/import_bitbucket_server.rb
+++ b/lib/api/import_bitbucket_server.rb
@@ -4,6 +4,10 @@ module API
class ImportBitbucketServer < ::API::Base
feature_category :importers
+ before do
+ forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('bitbucket_server')
+ end
+
helpers do
def client
@client ||= BitbucketServer::Client.new(credentials)
diff --git a/lib/api/integrations.rb b/lib/api/integrations.rb
new file mode 100644
index 00000000000..926cde340a0
--- /dev/null
+++ b/lib/api/integrations.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+module API
+ class Integrations < ::API::Base
+ feature_category :integrations
+
+ integrations = Helpers::IntegrationsHelpers.integrations
+ integration_classes = Helpers::IntegrationsHelpers.integration_classes
+
+ if Rails.env.development?
+ integrations['mock-ci'] = [
+ {
+ required: true,
+ name: :mock_service_url,
+ type: String,
+ desc: 'URL to the mock integration'
+ }
+ ]
+ integrations['mock-deployment'] = []
+ integrations['mock-monitoring'] = []
+
+ integration_classes += Helpers::IntegrationsHelpers.development_integration_classes
+ end
+
+ INTEGRATIONS = integrations.freeze
+
+ integration_classes.each do |integration|
+ event_names = integration.try(:event_names) || next
+ event_names.each do |event_name|
+ INTEGRATIONS[integration.to_param.tr("_", "-")] << {
+ required: false,
+ name: event_name.to_sym,
+ type: String,
+ desc: IntegrationsHelper.integration_event_description(integration, event_name)
+ }
+ end
+ end
+
+ TRIGGER_INTEGRATIONS = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ]
+ }.freeze
+
+ helpers do
+ def integration_attributes(integration)
+ integration.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
+ end
+ end
+ end
+
+ # The API officially documents only the `:id/integrations` API paths.
+ # We support the older `id:/services` path for backwards-compatibility in API V4.
+ # The support for `:id/services` can be dropped if we create an API V5.
+ [':id/services', ':id/integrations'].each do |path|
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ desc 'Get all active project integrations' do
+ success Entities::ProjectIntegrationBasic
+ end
+ get path do
+ integrations = user_project.integrations.active
+
+ present integrations, with: Entities::ProjectIntegrationBasic
+ end
+
+ INTEGRATIONS.each do |slug, settings|
+ desc "Set #{slug} integration for project"
+ params do
+ settings.each do |setting|
+ if setting[:required]
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ else
+ optional setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ end
+ put "#{path}/#{slug}" do
+ integration = user_project.find_or_initialize_integration(slug.underscore)
+ params = declared_params(include_missing: false).merge(active: true)
+
+ if integration.update(params)
+ present integration, with: Entities::ProjectIntegration
+ else
+ render_api_error!('400 Bad Request', 400)
+ end
+ end
+ end
+
+ desc "Delete an integration from a project"
+ params do
+ requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the integration'
+ end
+ delete "#{path}/:slug" do
+ integration = user_project.find_or_initialize_integration(params[:slug].underscore)
+
+ destroy_conditionally!(integration) do
+ attrs = integration_attributes(integration).index_with { nil }.merge(active: false)
+
+ render_api_error!('400 Bad Request', 400) unless integration.update(attrs)
+ end
+ end
+
+ desc 'Get the integration settings for a project' do
+ success Entities::ProjectIntegration
+ end
+ params do
+ requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the integration'
+ end
+ get "#{path}/:slug" do
+ integration = user_project.find_or_initialize_integration(params[:slug].underscore)
+
+ not_found!('Integration') unless integration&.persisted?
+
+ present integration, with: Entities::ProjectIntegration
+ end
+ end
+
+ TRIGGER_INTEGRATIONS.each do |integration_slug, settings|
+ helpers do
+ def slash_command_integration(project, integration_slug, params)
+ project.integrations.active.find do |integration|
+ integration.try(:token) == params[:token] && integration.to_param == integration_slug.underscore
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "Trigger a slash command for #{integration_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ post "#{path}/#{integration_slug.underscore}/trigger" do
+ project = find_project(params[:id])
+
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Integration') unless project
+
+ integration = slash_command_integration(project, integration_slug, params)
+ result = integration.try(:trigger, params)
+
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Integration')
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+API::Integrations.prepend_mod_with('API::Integrations')
diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb
index 1f437ad5bd3..5cade301d81 100644
--- a/lib/api/invitations.rb
+++ b/lib/api/invitations.rb
@@ -46,6 +46,8 @@ module API
source = find_source(source_type, params[:id])
query = params[:query]
+ authorize_admin_source!(source_type, source)
+
invitations = paginate(retrieve_member_invitations(source, query))
present_member_invitations invitations
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a92d904be84..34e0b528ced 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -89,6 +89,10 @@ module API
Gitlab::AppLogger.info({ message: "File exceeds maximum size", file_bytes: file.size, project_id: user_project.id, project_path: user_project.full_path, upload_allowed: allowed })
end
end
+
+ def check_import_by_url_is_enabled
+ forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('git')
+ end
end
helpers do
@@ -267,6 +271,7 @@ module API
attrs = declared_params(include_missing: false)
attrs = translate_params_for_compatibility(attrs)
filter_attributes_using_license!(attrs)
+ check_import_by_url_is_enabled if params[:import_url].present?
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
diff --git a/lib/api/services.rb b/lib/api/services.rb
deleted file mode 100644
index a37b6f4626a..00000000000
--- a/lib/api/services.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-# frozen_string_literal: true
-module API
- class Services < ::API::Base
- feature_category :integrations
-
- integrations = Helpers::IntegrationsHelpers.integrations
- integration_classes = Helpers::IntegrationsHelpers.integration_classes
-
- if Rails.env.development?
- integrations['mock-ci'] = [
- {
- required: true,
- name: :mock_service_url,
- type: String,
- desc: 'URL to the mock service'
- }
- ]
- integrations['mock-deployment'] = []
- integrations['mock-monitoring'] = []
-
- integration_classes += Helpers::IntegrationsHelpers.development_integration_classes
- end
-
- INTEGRATIONS = integrations.freeze
-
- integration_classes.each do |integration|
- event_names = integration.try(:event_names) || next
- event_names.each do |event_name|
- INTEGRATIONS[integration.to_param.tr("_", "-")] << {
- required: false,
- name: event_name.to_sym,
- type: String,
- desc: IntegrationsHelper.integration_event_description(integration, event_name)
- }
- end
- end
-
- TRIGGER_INTEGRATIONS = {
- 'mattermost-slash-commands' => [
- {
- name: :token,
- type: String,
- desc: 'The Mattermost token'
- }
- ],
- 'slack-slash-commands' => [
- {
- name: :token,
- type: String,
- desc: 'The Slack token'
- }
- ]
- }.freeze
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- before { authenticate! }
- before { authorize_admin_project }
-
- helpers do
- def integration_attributes(integration)
- integration.fields.inject([]) do |arr, hash|
- arr << hash[:name].to_sym
- end
- end
- end
-
- desc 'Get all active project integrations' do
- success Entities::ProjectIntegrationBasic
- end
- get ":id/services" do
- integrations = user_project.integrations.active
-
- present integrations, with: Entities::ProjectIntegrationBasic
- end
-
- INTEGRATIONS.each do |slug, settings|
- desc "Set #{slug} integration for project"
- params do
- settings.each do |setting|
- if setting[:required]
- requires setting[:name], type: setting[:type], desc: setting[:desc]
- else
- optional setting[:name], type: setting[:type], desc: setting[:desc]
- end
- end
- end
- put ":id/services/#{slug}" do
- integration = user_project.find_or_initialize_integration(slug.underscore)
- params = declared_params(include_missing: false).merge(active: true)
-
- if integration.update(params)
- present integration, with: Entities::ProjectIntegration
- else
- render_api_error!('400 Bad Request', 400)
- end
- end
- end
-
- desc "Delete an integration from a project"
- params do
- requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service'
- end
- delete ":id/services/:slug" do
- integration = user_project.find_or_initialize_integration(params[:slug].underscore)
-
- destroy_conditionally!(integration) do
- attrs = integration_attributes(integration).index_with { nil }.merge(active: false)
-
- render_api_error!('400 Bad Request', 400) unless integration.update(attrs)
- end
- end
-
- desc 'Get the integration settings for a project' do
- success Entities::ProjectIntegration
- end
- params do
- requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service'
- end
- get ":id/services/:slug" do
- integration = user_project.find_or_initialize_integration(params[:slug].underscore)
-
- not_found!('Service') unless integration&.persisted?
-
- present integration, with: Entities::ProjectIntegration
- end
- end
-
- TRIGGER_INTEGRATIONS.each do |integration_slug, settings|
- helpers do
- def slash_command_integration(project, integration_slug, params)
- project.integrations.active.find do |integration|
- integration.try(:token) == params[:token] && integration.to_param == integration_slug.underscore
- end
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc "Trigger a slash command for #{integration_slug}" do
- detail 'Added in GitLab 8.13'
- end
- params do
- settings.each do |setting|
- requires setting[:name], type: setting[:type], desc: setting[:desc]
- end
- end
- post ":id/services/#{integration_slug.underscore}/trigger" do
- project = find_project(params[:id])
-
- # This is not accurate, but done to prevent leakage of the project names
- not_found!('Service') unless project
-
- integration = slash_command_integration(project, integration_slug, params)
- result = integration.try(:trigger, params)
-
- if result
- status result[:status] || 200
- present result
- else
- not_found!('Service')
- end
- end
- end
- end
- end
-end
-
-API::Services.prepend_mod_with('API::Services')
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 1cab668a248..74fa98128e8 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -140,7 +140,10 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id", feature_category: :users do
+ forbidden!('Not authorized!') unless current_user
+
user = User.find_by(id: params[:id])
+
not_found!('User') unless user && can?(current_user, :read_user, user)
opts = { with: current_user&.admin? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user }
@@ -156,6 +159,7 @@ module API
end
get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users do
user = find_user(params[:user_id])
+
not_found!('User') unless user && can?(current_user, :read_user, user)
present user.status || {}, with: Entities::UserStatus
@@ -203,6 +207,8 @@ module API
use :pagination
end
get ':id/following', feature_category: :users do
+ forbidden!('Not authorized!') unless current_user
+
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
@@ -217,6 +223,8 @@ module API
use :pagination
end
get ':id/followers', feature_category: :users do
+ forbidden!('Not authorized!') unless current_user
+
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index ca26e6d1581..f8d03fd6e50 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -26,14 +26,17 @@ module Banzai
# Pattern to match a standard markdown link
#
# Rubular: http://rubular.com/r/2EXEQ49rg5
- LINK_OR_IMAGE_PATTERN = %r{
- (?<preview_operator>!)?
- \[(?<text>.+?)\]
- \(
- (?<new_link>.+?)
- (?<title>\ ".+?")?
- \)
- }x.freeze
+ #
+ # This pattern is vulnerable to malicious inputs, so use Gitlab::UntrustedRegexp
+ # to place bounds on execution time
+ LINK_OR_IMAGE_PATTERN = Gitlab::UntrustedRegexp.new(
+ '(?P<preview_operator>!)?' \
+ '\[(?P<text>.+?)\]' \
+ '\(' \
+ '(?P<new_link>.+?)' \
+ '(?P<title>\ ".+?")?' \
+ '\)'
+ )
# Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
@@ -48,7 +51,7 @@ module Banzai
doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html
- next unless content.match(LINK_OR_IMAGE_PATTERN)
+ next unless LINK_OR_IMAGE_PATTERN.match(content)
html = spaced_link_filter(content)
diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb
index 93fd6986173..f01ce22a46d 100644
--- a/lib/bulk_imports/ndjson_pipeline.rb
+++ b/lib/bulk_imports/ndjson_pipeline.rb
@@ -14,7 +14,7 @@ module BulkImports
relation_definition = import_export_config.top_relation_tree(relation)
deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash|
- Gitlab::ImportExport::Group::RelationFactory.create(
+ relation_factory.create(
relation_index: relation_index,
relation_sym: key.to_sym,
relation_hash: hash,
@@ -83,6 +83,10 @@ module BulkImports
"Gitlab::ImportExport::#{portable.class}::ObjectBuilder".constantize
end
+ def relation_factory
+ "Gitlab::ImportExport::#{portable.class}::RelationFactory".constantize
+ end
+
def relation
self.class.relation
end
diff --git a/lib/bulk_imports/projects/pipelines/issues_pipeline.rb b/lib/bulk_imports/projects/pipelines/issues_pipeline.rb
new file mode 100644
index 00000000000..5d5bd58f1eb
--- /dev/null
+++ b/lib/bulk_imports/projects/pipelines/issues_pipeline.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Projects
+ module Pipelines
+ class IssuesPipeline
+ include NdjsonPipeline
+
+ relation_name 'issues'
+
+ extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/projects/stage.rb b/lib/bulk_imports/projects/stage.rb
index eb5e3c9cb0a..26b9b02efd1 100644
--- a/lib/bulk_imports/projects/stage.rb
+++ b/lib/bulk_imports/projects/stage.rb
@@ -15,9 +15,13 @@ module BulkImports
pipeline: BulkImports::Common::Pipelines::LabelsPipeline,
stage: 1
},
+ issues: {
+ pipeline: BulkImports::Projects::Pipelines::IssuesPipeline,
+ stage: 2
+ },
finisher: {
pipeline: BulkImports::Common::Pipelines::EntityFinisher,
- stage: 2
+ stage: 3
}
}
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 1afb2eda149..0970b92723b 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -172,7 +172,11 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
- raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
+ verifier = TwoFactorAuthVerifier.new(user)
+
+ if user.two_factor_enabled? || verifier.two_factor_authentication_enforced?
+ raise Gitlab::Auth::MissingPersonalAccessTokenError
+ end
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index f6ee08defcf..9c33a5fc872 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -342,6 +342,10 @@ module Gitlab
Gitlab::PathRegex.repository_git_lfs_route_regex.match?(current_request.path)
end
+ def git_or_lfs_request?
+ git_request? || git_lfs_request?
+ end
+
def archive_request?
current_request.path.include?('/-/archive/')
end
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index dfc682e8a5c..08214bbd449 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -35,13 +35,31 @@ module Gitlab
find_user_from_static_object_token(request_format) ||
find_user_from_basic_auth_job ||
find_user_from_job_token ||
- find_user_from_lfs_token ||
- find_user_from_personal_access_token ||
- find_user_from_basic_auth_password
+ find_user_from_personal_access_token_for_api_or_git ||
+ find_user_for_git_or_lfs_request
rescue Gitlab::Auth::AuthenticationError
nil
end
+ # To prevent Rack Attack from incorrectly rate limiting
+ # authenticated Git activity, we need to authenticate the user
+ # from other means (e.g. HTTP Basic Authentication) only if the
+ # request originated from a Git or Git LFS
+ # request. Repositories::GitHttpClientController or
+ # Repositories::LfsApiController normally does the authentication,
+ # but Rack Attack runs before those controllers.
+ def find_user_for_git_or_lfs_request
+ return unless git_or_lfs_request?
+
+ find_user_from_lfs_token || find_user_from_basic_auth_password
+ end
+
+ def find_user_from_personal_access_token_for_api_or_git
+ return unless api_request? || git_or_lfs_request?
+
+ find_user_from_personal_access_token
+ end
+
def valid_access_token?(scopes: [])
validate_access_token!(scopes: scopes)
diff --git a/lib/gitlab/auth/two_factor_auth_verifier.rb b/lib/gitlab/auth/two_factor_auth_verifier.rb
index 86552ef1267..5a203a1fe9c 100644
--- a/lib/gitlab/auth/two_factor_auth_verifier.rb
+++ b/lib/gitlab/auth/two_factor_auth_verifier.rb
@@ -9,6 +9,10 @@ module Gitlab
@current_user = current_user
end
+ def two_factor_authentication_enforced?
+ two_factor_authentication_required? && two_factor_grace_period_expired?
+ end
+
def two_factor_authentication_required?
Gitlab::CurrentSettings.require_two_factor_authentication? ||
current_user&.require_two_factor_authentication_from_group?
diff --git a/lib/gitlab/fogbugz_import.rb b/lib/gitlab/fogbugz_import.rb
new file mode 100644
index 00000000000..a4a52edd83e
--- /dev/null
+++ b/lib/gitlab/fogbugz_import.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'fogbugz'
+
+module Gitlab
+ module FogbugzImport
+ # Custom adapter to validate the URL before each request
+ # This way we avoid DNS rebinds or other unsafe requests
+ ::Fogbugz.adapter[:http] = HttpAdapter
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index dd747a79673..024c1ae0439 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'fogbugz'
-
module Gitlab
module FogbugzImport
class Client
diff --git a/lib/gitlab/fogbugz_import/http_adapter.rb b/lib/gitlab/fogbugz_import/http_adapter.rb
new file mode 100644
index 00000000000..bfae7a10f5b
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/http_adapter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module FogbugzImport
+ class HttpAdapter
+ def initialize(options = {})
+ @root_url = options[:uri]
+ end
+
+ def request(action, options = {})
+ uri = Gitlab::Utils.append_path(@root_url, 'api.asp')
+
+ params = { 'cmd' => action }.merge(options.fetch(:params, {}))
+
+ response = Gitlab::HTTP.post(uri, body: params)
+
+ response.body
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/redis/rate_limiting_check.rb b/lib/gitlab/health_checks/redis/rate_limiting_check.rb
new file mode 100644
index 00000000000..67c14e26361
--- /dev/null
+++ b/lib/gitlab/health_checks/redis/rate_limiting_check.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HealthChecks
+ module Redis
+ class RateLimitingCheck
+ extend SimpleAbstractCheck
+
+ class << self
+ def check_up
+ check
+ end
+
+ private
+
+ def metric_prefix
+ 'redis_rate_limiting_ping'
+ end
+
+ def successful?(result)
+ result == 'PONG'
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def check
+ catch_timeout 10.seconds do
+ Gitlab::Redis::RateLimiting.with(&:ping)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/redis/redis_check.rb b/lib/gitlab/health_checks/redis/redis_check.rb
index 44b85bf886e..2fa39308b9a 100644
--- a/lib/gitlab/health_checks/redis/redis_check.rb
+++ b/lib/gitlab/health_checks/redis/redis_check.rb
@@ -21,7 +21,8 @@ module Gitlab
::Gitlab::HealthChecks::Redis::CacheCheck.check_up &&
::Gitlab::HealthChecks::Redis::QueuesCheck.check_up &&
::Gitlab::HealthChecks::Redis::SharedStateCheck.check_up &&
- ::Gitlab::HealthChecks::Redis::TraceChunksCheck.check_up
+ ::Gitlab::HealthChecks::Redis::TraceChunksCheck.check_up &&
+ ::Gitlab::HealthChecks::Redis::RateLimitingCheck.check_up
end
end
end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index 630f918a78b..f7ab1677001 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -37,6 +37,7 @@ excluded_attributes:
- :trial_ends_on
- :shared_runners_minute_limit
- :extra_shared_runners_minutes_limit
+ - :repository_size_limit
epics:
- :state_id
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index fe0974d27a6..8046fedc4f3 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -88,7 +88,6 @@ tree:
- :external_pull_request
- :merge_request
- :auto_devops
- - :triggers
- :pipeline_schedules
- :container_expiration_policy
- protected_branches:
@@ -211,6 +210,7 @@ excluded_attributes:
- :show_default_award_emojis
- :services
- :exported_protected_branches
+ - :repository_size_limit
namespaces:
- :runners_token
- :runners_token_encrypted
diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb
index ab0e56adc32..ea1d54ff867 100644
--- a/lib/gitlab/instrumentation/redis.rb
+++ b/lib/gitlab/instrumentation/redis.rb
@@ -9,8 +9,9 @@ module Gitlab
Queues = Class.new(RedisBase)
SharedState = Class.new(RedisBase).enable_redis_cluster_validation
TraceChunks = Class.new(RedisBase).enable_redis_cluster_validation
+ RateLimiting = Class.new(RedisBase).enable_redis_cluster_validation
- STORAGES = [ActionCable, Cache, Queues, SharedState, TraceChunks].freeze
+ STORAGES = [ActionCable, Cache, Queues, SharedState, TraceChunks, RateLimiting].freeze
# Milliseconds represented in seconds (from 1 millisecond to 2 seconds).
QUERY_TIME_BUCKETS = [0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2].freeze
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
index 4482610523e..48a8e0ce6d7 100644
--- a/lib/gitlab/legacy_github_import/client.rb
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -8,9 +8,10 @@ module Gitlab
attr_reader :access_token, :host, :api_version, :wait_for_rate_limit_reset
- def initialize(access_token, host: nil, api_version: 'v3', wait_for_rate_limit_reset: true)
+ def initialize(access_token, host: nil, api_version: 'v3', wait_for_rate_limit_reset: true, hostname: nil)
@access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
+ @hostname = hostname
@api_version = api_version
@users = {}
@wait_for_rate_limit_reset = wait_for_rate_limit_reset
@@ -28,7 +29,8 @@ module Gitlab
# If there is no config, we're connecting to github.com and we
# should verify ssl.
connection_options: {
- ssl: { verify: config ? config['verify_ssl'] : true }
+ ssl: { verify: config ? config['verify_ssl'] : true },
+ headers: { host: @hostname }.compact
}
)
end
diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb
index 641c0c76f8f..6d4b49676e5 100644
--- a/lib/gitlab/metrics/dashboard/service_selector.rb
+++ b/lib/gitlab/metrics/dashboard/service_selector.rb
@@ -30,7 +30,7 @@ module Gitlab
# Returns a class which inherits from the BaseService
# class that can be used to obtain a dashboard for
# the provided params.
- # @return [Gitlab::Metrics::Dashboard::Services::BaseService]
+ # @return [Metrics::Dashboard::BaseService]
def call(params)
service = services.find do |service_class|
service_class.valid_params?(params)
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index 98b66080b42..a2c7b5e29db 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -5,12 +5,15 @@ module Gitlab
class Cache < ::Gitlab::Redis::Wrapper
CACHE_NAMESPACE = 'cache:gitlab'
- private
-
- def raw_config_hash
- config = super
- config[:url] = 'redis://localhost:6380' if config[:url].blank?
- config
+ # Full list of options:
+ # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
+ def self.active_support_config
+ {
+ redis: pool,
+ compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
+ namespace: CACHE_NAMESPACE,
+ expires_in: 2.weeks # Cache should not grow forever
+ }
end
end
end
diff --git a/lib/gitlab/redis/queues.rb b/lib/gitlab/redis/queues.rb
index 9e291a73bb6..e60e59dcf01 100644
--- a/lib/gitlab/redis/queues.rb
+++ b/lib/gitlab/redis/queues.rb
@@ -2,21 +2,12 @@
# We need this require for MailRoom
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
-require 'active_support/core_ext/object/blank'
module Gitlab
module Redis
class Queues < ::Gitlab::Redis::Wrapper
SIDEKIQ_NAMESPACE = 'resque:gitlab'
MAILROOM_NAMESPACE = 'mail_room:gitlab'
-
- private
-
- def raw_config_hash
- config = super
- config[:url] = 'redis://localhost:6381' if config[:url].blank?
- config
- end
end
end
end
diff --git a/lib/gitlab/redis/rate_limiting.rb b/lib/gitlab/redis/rate_limiting.rb
new file mode 100644
index 00000000000..44681b4d5be
--- /dev/null
+++ b/lib/gitlab/redis/rate_limiting.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Redis
+ class RateLimiting < ::Gitlab::Redis::Wrapper
+ # The data we store on RateLimiting used to be stored on Cache.
+ def self.config_fallback
+ Cache
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index d62516bd287..1250eabb041 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -7,14 +7,6 @@ module Gitlab
USER_SESSIONS_NAMESPACE = 'session:user:gitlab'
USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'
IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2'
-
- private
-
- def raw_config_hash
- config = super
- config[:url] = 'redis://localhost:6382' if config[:url].blank?
- config
- end
end
end
end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index 3c8ac07215d..db1e24a3201 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -6,6 +6,7 @@
# Rails.
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/module/delegation'
+require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
# Explicitly load Redis::Store::Factory so we can read Redis configuration in
@@ -150,11 +151,35 @@ module Gitlab
def raw_config_hash
config_data = fetch_config
- if config_data
- config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys
- else
- { url: '' }
+ config_hash =
+ if config_data
+ config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys
+ else
+ { url: '' }
+ end
+
+ if config_hash[:url].blank?
+ config_hash[:url] = legacy_fallback_urls[self.class.store_name] || legacy_fallback_urls[self.class.config_fallback.store_name]
end
+
+ config_hash
+ end
+
+ # These URLs were defined for cache, queues, and shared_state in
+ # code. They are used only when no config file exists at all for a
+ # given instance. The configuration does not seem particularly
+ # useful - it uses different ports on localhost - but we cannot
+ # confidently delete it as we don't know if any instances rely on
+ # this.
+ #
+ # DO NOT ADD new instances here. All new instances should define a
+ # `.config_fallback`, which will then be used to look up this URL.
+ def legacy_fallback_urls
+ {
+ 'Cache' => 'redis://localhost:6380',
+ 'Queues' => 'redis://localhost:6381',
+ 'SharedState' => 'redis://localhost:6382'
+ }
end
def fetch_config
diff --git a/lib/gitlab/string_regex_marker.rb b/lib/gitlab/string_regex_marker.rb
index f1982ff914c..8e0167a433e 100644
--- a/lib/gitlab/string_regex_marker.rb
+++ b/lib/gitlab/string_regex_marker.rb
@@ -2,18 +2,20 @@
module Gitlab
class StringRegexMarker < StringRangeMarker
- # rubocop: disable CodeReuse/ActiveRecord
def mark(regex, group: 0, &block)
ranges = []
+ offset = 0
- raw_line.scan(regex) do
- begin_index, end_index = Regexp.last_match.offset(group)
+ while match = regex.match(raw_line[offset..])
+ begin_index = match.begin(group) + offset
+ end_index = match.end(group) + offset
ranges << (begin_index..(end_index - 1))
+
+ offset = end_index
end
super(ranges, &block)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/lib/tasks/frontend.rake b/lib/tasks/frontend.rake
index b2d2c4e3f2b..95f26cc0800 100644
--- a/lib/tasks/frontend.rake
+++ b/lib/tasks/frontend.rake
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+require 'yaml'
unless Rails.env.production?
namespace :frontend do
@@ -13,12 +14,28 @@ unless Rails.env.production?
t.rspec_opts = '--format documentation'
end
+ desc 'GitLab | Frontend | Generate fixtures for JavaScript integration tests'
+ RSpec::Core::RakeTask.new(:mock_server_rspec_fixtures) do |t, args|
+ base_path = Pathname.new('spec/frontend_integration/fixture_generators.yml')
+ ee_path = Pathname.new('ee') + base_path
+
+ fixtures = YAML.safe_load(base_path.read)
+ fixtures.concat(Array(YAML.safe_load(ee_path.read))) if Gitlab.ee? && ee_path.exist?
+
+ t.pattern = fixtures.join(',')
+ ENV['NO_KNAPSACK'] = 'true'
+ t.rspec_opts = '--format documentation'
+ end
+
desc 'GitLab | Frontend | Run JavaScript tests'
task tests: ['yarn:check'] do
sh "yarn test" do |ok, res|
abort('rake frontend:tests failed') unless ok
end
end
+
+ desc 'GitLab | Frontend | Shortcut for generating all fixtures used by MirajeJS mock server'
+ task mock_server_fixtures: ['frontend:mock_server_rspec_fixtures', 'gitlab:graphql:schema:dump']
end
desc 'GitLab | Frontend | Shortcut for frontend:fixtures and frontend:tests'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f32e19b726a..b5b3932a70b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2285,6 +2285,9 @@ msgstr ""
msgid "AdminArea|Features"
msgstr ""
+msgid "AdminArea|Get security updates from GitLab and stay up to date"
+msgstr ""
+
msgid "AdminArea|Groups"
msgstr ""
@@ -2324,6 +2327,12 @@ msgstr ""
msgid "AdminArea|Reporter"
msgstr ""
+msgid "AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates."
+msgstr ""
+
+msgid "AdminArea|Sign up for the GitLab newsletter"
+msgstr ""
+
msgid "AdminArea|Stop all jobs"
msgstr ""
@@ -38575,6 +38584,9 @@ msgstr ""
msgid "You are already a member of this %{member_source}."
msgstr ""
+msgid "You are already impersonating another user"
+msgstr ""
+
msgid "You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
@@ -39289,6 +39301,9 @@ msgstr ""
msgid "Your commit email is used for web based operations, such as edits and merges."
msgstr ""
+msgid "Your current password is required to register a two-factor authenticator app."
+msgstr ""
+
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr ""
diff --git a/package.json b/package.json
index 5f2c72beea2..6dee235a575 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"postinstall": "node ./scripts/frontend/postinstall.js",
"storybook:install": "yarn --cwd ./storybook install",
"storybook:build": "yarn --cwd ./storybook build",
- "storybook:start": "yarn --cwd ./storybook start",
+ "storybook:start": "./scripts/frontend/start_storybook.sh",
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
"webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.vendor.config.js",
diff --git a/qa/Gemfile b/qa/Gemfile
index 9b1735fe646..aae3c81bd83 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -26,7 +26,7 @@ gem 'webdrivers', '~> 4.6'
gem 'zeitwerk', '~> 2.4'
gem 'influxdb-client', '~> 1.17'
-gem 'chemlab', '~> 0.8'
+gem 'chemlab', '~> 0.9'
gem 'chemlab-library-www-gitlab-com', '~> 0.1'
gem 'deprecation_toolkit', '~> 1.5.1', require: false
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index ded3abe74b4..f105274b3ac 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -41,7 +41,7 @@ GEM
capybara-screenshot (1.0.23)
capybara (>= 1.0, < 4)
launchy
- chemlab (0.8.1)
+ chemlab (0.9.1)
colorize (~> 0.8)
i18n (~> 1.8)
rake (>= 12, < 14)
@@ -244,7 +244,7 @@ DEPENDENCIES
allure-rspec (~> 2.14.5)
capybara (~> 3.35.0)
capybara-screenshot (~> 1.0.23)
- chemlab (~> 0.8)
+ chemlab (~> 0.9)
chemlab-library-www-gitlab-com (~> 0.1)
deprecation_toolkit (~> 1.5.1)
faker (~> 2.19, >= 2.19.0)
diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec
index d42d0e5e926..34a55ba8927 100644
--- a/qa/chemlab-library-gitlab.gemspec
+++ b/qa/chemlab-library-gitlab.gemspec
@@ -4,7 +4,7 @@ $:.unshift(File.expand_path('lib', __dir__))
Gem::Specification.new do |spec|
spec.name = 'chemlab-library-gitlab'
- spec.version = '0.2.0'
+ spec.version = '0.3.0'
spec.authors = ['GitLab Quality']
spec.email = ['quality@gitlab.com']
@@ -18,5 +18,5 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']
- spec.add_runtime_dependency 'chemlab', '~> 0.8'
+ spec.add_runtime_dependency 'chemlab', '~> 0.9'
end
diff --git a/qa/lib/gitlab/page/main/login.rb b/qa/lib/gitlab/page/main/login.rb
index 9f20a040550..de05df1a086 100644
--- a/qa/lib/gitlab/page/main/login.rb
+++ b/qa/lib/gitlab/page/main/login.rb
@@ -10,11 +10,27 @@ module Gitlab
text_field :password_field
button :sign_in_button
- def sign_in_as(username:, password:)
+ button :accept_terms, text: 'Accept terms'
+
+ # password change tab
+ text_field :password_confirmation_field
+ button :change_password_button
+
+ # Sign in using a given username and password
+ # @note this will also automatically accept terms if prompted
+ # @param [String] username the username to sign in with
+ # @param [String] password the password to sign in with
+ # @example
+ # Page::Main::Login.perform do |login|
+ # login.sign_in_as(username: 'username', password: 'password')
+ # login.sign_in_as(username: 'username', password: 'password', accept_terms: false)
+ # end
+ def sign_in_as(username:, password:, accept_terms: true)
self.login_field = username
self.password_field = password
sign_in_button
+ self.accept_terms if accept_terms && accept_terms?
end
end
end
diff --git a/qa/lib/gitlab/page/main/login.stub.rb b/qa/lib/gitlab/page/main/login.stub.rb
index a4cef291616..a819ca4bcc8 100644
--- a/qa/lib/gitlab/page/main/login.stub.rb
+++ b/qa/lib/gitlab/page/main/login.stub.rb
@@ -95,6 +95,88 @@ module Gitlab
def sign_in_button?
# This is a stub, used for indexing. The method is dynamically generated.
end
+
+ # @note Defined as +button :accept_terms+
+ # Clicks +accept_terms+
+ def accept_terms
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login.accept_terms_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def accept_terms_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login).to be_accept_terms
+ # end
+ # @return [Boolean] true if the +accept_terms+ element is present on the page
+ def accept_terms?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +text_field :password_confirmation_field+
+ # @return [String] The text content or value of +password_confirmation_field+
+ def password_confirmation_field
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # Set the value of password_confirmation_field
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # login.password_confirmation_field = 'value'
+ # end
+ # @param value [String] The value to set.
+ def password_confirmation_field=(value)
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login.password_confirmation_field_element).to exist
+ # end
+ # @return [Watir::TextField] The raw +TextField+ element
+ def password_confirmation_field_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login).to be_password_confirmation_field
+ # end
+ # @return [Boolean] true if the +password_confirmation_field+ element is present on the page
+ def password_confirmation_field?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +button :change_password_button+
+ # Clicks +change_password_button+
+ def change_password_button
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login.change_password_button_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def change_password_button_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Login.perform do |login|
+ # expect(login).to be_change_password_button
+ # end
+ # @return [Boolean] true if the +change_password_button+ element is present on the page
+ def change_password_button?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
end
end
end
diff --git a/scripts/frontend/start_storybook.sh b/scripts/frontend/start_storybook.sh
new file mode 100755
index 00000000000..7ae0a21b15b
--- /dev/null
+++ b/scripts/frontend/start_storybook.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+bold=$(tput bold)
+normal=$(tput sgr0)
+
+echo -e "Storybook provides a mock server that allows creating stories for components that make HTTP requests."
+echo -e "${bold}Storybook will fail to start if it can’t find the fixtures used by the mock server.${normal}\n"
+read -rp "Would you like to generate/update the frontend fixtures used by the mock server (y/N)? " answer
+
+if [[ "$answer" =~ ^(Y|y)$ ]] ; then
+ bundle exec rake frontend:mock_server_fixtures
+fi
+
+if ! [[ -d storybook/node_modules ]] ; then
+ yarn storybook:install
+fi
+
+yarn --cwd ./storybook start
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index f3b9ac56082..4b528696322 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -38,18 +38,6 @@ sed -i 's|url:.*$|url: redis://redis:6379|g' config/cable.yml
cp config/resque.yml.example config/resque.yml
sed -i 's|url:.*$|url: redis://redis:6379|g' config/resque.yml
-cp config/redis.cache.yml.example config/redis.cache.yml
-sed -i 's|url:.*$|url: redis://redis:6379/10|g' config/redis.cache.yml
-
-cp config/redis.queues.yml.example config/redis.queues.yml
-sed -i 's|url:.*$|url: redis://redis:6379/11|g' config/redis.queues.yml
-
-cp config/redis.shared_state.yml.example config/redis.shared_state.yml
-sed -i 's|url:.*$|url: redis://redis:6379/12|g' config/redis.shared_state.yml
-
-cp config/redis.trace_chunks.yml.example config/redis.trace_chunks.yml
-sed -i 's|url:.*$|url: redis://redis:6379/13|g' config/redis.trace_chunks.yml
-
if [ "$SETUP_DB" != "false" ]; then
setup_db
elif getent hosts postgres; then
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index 744c0712d6b..ccf4454c349 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -92,6 +92,14 @@ RSpec.describe Admin::ImpersonationsController do
expect(warden.user).to eq(impersonator)
end
+
+ it 'clears token session keys' do
+ session[:bitbucket_token] = SecureRandom.hex(8)
+
+ delete :destroy
+
+ expect(session[:bitbucket_token]).to be_nil
+ end
end
# base case
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 015c36c9335..3a2b5dcb99d 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -794,6 +794,14 @@ RSpec.describe Admin::UsersController do
expect(flash[:alert]).to eq("You are now impersonating #{user.username}")
end
+
+ it 'clears token session keys' do
+ session[:github_access_token] = SecureRandom.hex(8)
+
+ post :impersonate, params: { id: user.username }
+
+ expect(session[:github_access_token]).to be_nil
+ end
end
context "when impersonation is disabled" do
@@ -807,5 +815,20 @@ RSpec.describe Admin::UsersController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when impersonating an admin and attempting to impersonate again' do
+ let(:admin2) { create(:admin) }
+
+ before do
+ post :impersonate, params: { id: admin2.username }
+ end
+
+ it 'does not allow double impersonation', :aggregate_failures do
+ post :impersonate, params: { id: user.username }
+
+ expect(flash[:alert]).to eq(_('You are already impersonating another user'))
+ expect(warden.user).to eq(admin2)
+ end
+ end
end
end
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb
index 3e4b159271a..568712d29cb 100644
--- a/spec/controllers/import/gitea_controller_spec.rb
+++ b/spec/controllers/import/gitea_controller_spec.rb
@@ -54,6 +54,48 @@ RSpec.describe Import::GiteaController do
end
end
end
+
+ context 'when DNS Rebinding protection is enabled' do
+ let(:token) { 'gitea token' }
+
+ let(:ip_uri) { 'http://167.99.148.217' }
+ let(:uri) { 'try.gitea.io' }
+ let(:https_uri) { "https://#{uri}" }
+ let(:http_uri) { "http://#{uri}" }
+
+ before do
+ session[:gitea_access_token] = token
+
+ allow(Gitlab::UrlBlocker).to receive(:validate!).with(https_uri, anything).and_return([Addressable::URI.parse(https_uri), uri])
+ allow(Gitlab::UrlBlocker).to receive(:validate!).with(http_uri, anything).and_return([Addressable::URI.parse(ip_uri), uri])
+
+ allow(Gitlab::LegacyGithubImport::Client).to receive(:new).and_return(double('Gitlab::LegacyGithubImport::Client', repos: [], orgs: []))
+ end
+
+ context 'when provided host url is using https' do
+ let(:host_url) { https_uri }
+
+ it 'uses unchanged host url to send request to Gitea' do
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: https_uri, api_version: 'v1', hostname: 'try.gitea.io')
+
+ get :status, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when provided host url is using http' do
+ let(:host_url) { http_uri }
+
+ it 'uses changed host url to send request to Gitea' do
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: 'http://167.99.148.217', api_version: 'v1', hostname: 'try.gitea.io')
+
+ get :status, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index f21ef324884..5bf3b4c48bf 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -98,6 +98,19 @@ RSpec.describe Oauth::ApplicationsController do
end
describe 'POST #create' do
+ let(:oauth_params) do
+ {
+ doorkeeper_application: {
+ name: 'foo',
+ redirect_uri: redirect_uri,
+ scopes: scopes
+ }
+ }
+ end
+
+ let(:redirect_uri) { 'http://example.org' }
+ let(:scopes) { ['api'] }
+
subject { post :create, params: oauth_params }
it 'creates an application' do
@@ -116,38 +129,42 @@ RSpec.describe Oauth::ApplicationsController do
expect(response).to redirect_to(profile_path)
end
- context 'redirect_uri' do
+ context 'when redirect_uri is invalid' do
+ let(:redirect_uri) { 'javascript://alert()' }
+
render_views
it 'shows an error for a forbidden URI' do
- invalid_uri_params = {
- doorkeeper_application: {
- name: 'foo',
- redirect_uri: 'javascript://alert()',
- scopes: ['api']
- }
- }
-
- post :create, params: invalid_uri_params
+ subject
expect(response.body).to include 'Redirect URI is forbidden by the server'
+ expect(response).to render_template('doorkeeper/applications/index')
end
end
context 'when scopes are not present' do
+ let(:scopes) { [] }
+
render_views
it 'shows an error for blank scopes' do
- invalid_uri_params = {
- doorkeeper_application: {
- name: 'foo',
- redirect_uri: 'http://example.org'
- }
- }
-
- post :create, params: invalid_uri_params
+ subject
expect(response.body).to include 'Scopes can&#39;t be blank'
+ expect(response).to render_template('doorkeeper/applications/index')
+ end
+ end
+
+ context 'when scopes are invalid' do
+ let(:scopes) { %w(api foo) }
+
+ render_views
+
+ it 'shows an error for invalid scopes' do
+ subject
+
+ expect(response.body).to include 'Scopes doesn&#39;t match configured on the server.'
+ expect(response).to render_template('doorkeeper/applications/index')
end
end
@@ -185,14 +202,4 @@ RSpec.describe Oauth::ApplicationsController do
def disable_user_oauth
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
end
-
- def oauth_params
- {
- doorkeeper_application: {
- name: 'foo',
- redirect_uri: 'http://example.org',
- scopes: ['api']
- }
- }
- end
end
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 073180cbafd..a0e2cf671af 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -35,6 +35,27 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
+ shared_examples 'user must enter a valid current password' do
+ let(:current_password) { '123' }
+
+ it 'requires the current password', :aggregate_failures do
+ go
+
+ expect(response).to redirect_to(profile_two_factor_auth_path)
+ expect(flash[:alert]).to eq(_('You must provide a valid current password'))
+ end
+
+ context 'when the user is on the last sign in attempt' do
+ it do
+ user.update!(failed_attempts: User.maximum_attempts.pred)
+
+ go
+
+ expect(user.reload).to be_access_locked
+ end
+ end
+ end
+
describe 'GET show' do
let_it_be_with_reload(:user) { create(:user) }
@@ -69,9 +90,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do
let_it_be_with_reload(:user) { create(:user) }
let(:pin) { 'pin-code' }
+ let(:current_password) { user.password }
def go
- post :create, params: { pin_code: pin }
+ post :create, params: { pin_code: pin, current_password: current_password }
end
context 'with valid pin' do
@@ -136,21 +158,25 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
+ it_behaves_like 'user must enter a valid current password'
+
it_behaves_like 'user must first verify their primary email address'
end
describe 'POST codes' do
let_it_be_with_reload(:user) { create(:user, :two_factor) }
+ let(:current_password) { user.password }
+
it 'presents plaintext codes for the user to save' do
expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c))
- post :codes
+ post :codes, params: { current_password: current_password }
expect(assigns[:codes]).to match_array %w(a b c)
end
it 'persists the generated codes' do
- post :codes
+ post :codes, params: { current_password: current_password }
user.reload
expect(user.otp_backup_codes).not_to be_empty
@@ -159,12 +185,18 @@ RSpec.describe Profiles::TwoFactorAuthsController do
it 'dismisses the `TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK` callout' do
expect(controller.helpers).to receive(:dismiss_two_factor_auth_recovery_settings_check)
- post :codes
+ post :codes, params: { current_password: current_password }
+ end
+
+ it_behaves_like 'user must enter a valid current password' do
+ let(:go) { post :codes, params: { current_password: current_password } }
end
end
describe 'DELETE destroy' do
- subject { delete :destroy }
+ subject { delete :destroy, params: { current_password: current_password } }
+
+ let(:current_password) { user.password }
context 'for a user that has 2FA enabled' do
let_it_be_with_reload(:user) { create(:user, :two_factor) }
@@ -187,6 +219,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do
expect(flash[:notice])
.to eq _('Two-factor authentication has been disabled successfully!')
end
+
+ it_behaves_like 'user must enter a valid current password' do
+ let(:go) { delete :destroy, params: { current_password: current_password } }
+ end
end
context 'for a user that does not have 2FA enabled' do
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index be5c1f0d428..c352524ec14 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -624,9 +624,9 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- context 'when user can access source project members' do
+ context 'when user can admin source project members' do
before do
- another_project.add_guest(user)
+ another_project.add_maintainer(user)
end
include_context 'import applied'
@@ -640,7 +640,11 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- context 'when user is not member of a source project' do
+ context "when user can't admin source project members" do
+ before do
+ another_project.add_developer(user)
+ end
+
include_context 'import applied'
it 'does not import team members' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index a7aa0f1a1b8..2bb5fad9231 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -419,6 +419,47 @@ RSpec.describe ProjectsController do
end
end
+ describe 'POST create' do
+ let!(:params) do
+ {
+ path: 'foo',
+ description: 'bar',
+ import_url: project.http_url_to_repo,
+ namespace_id: user.namespace.id
+ }
+ end
+
+ subject { post :create, params: { project: params } }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when import by url is disabled' do
+ before do
+ stub_application_setting(import_sources: [])
+ end
+
+ it 'does not create project and reports an error' do
+ expect { subject }.not_to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when import by url is enabled' do
+ before do
+ stub_application_setting(import_sources: ['git'])
+ end
+
+ it 'creates project' do
+ expect { subject }.to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+
describe 'GET edit' do
it 'allows an admin user to access the page', :enable_admin_mode do
sign_in(create(:user, :admin))
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 043fd97f1ad..2aa9b86b20e 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -666,6 +666,6 @@ RSpec.describe UploadsController do
def post_authorize(verified: true)
request.headers.merge!(workhorse_internal_api_request_header) if verified
- post :authorize, params: { model: 'personal_snippet', id: model.id }, format: :json
+ post :authorize, params: params, format: :json
end
end
diff --git a/spec/features/callouts/security_newsletter_callout_spec.rb b/spec/features/callouts/security_newsletter_callout_spec.rb
new file mode 100644
index 00000000000..b17bb372456
--- /dev/null
+++ b/spec/features/callouts/security_newsletter_callout_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Security newsletter callout', :js do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:non_admin) { create(:user) }
+
+ shared_examples 'hidden callout' do
+ it 'does not display callout' do
+ expect(page).not_to have_content 'Sign up for the GitLab Security Newsletter to get notified for security updates.'
+ end
+ end
+
+ context 'when an admin is logged in' do
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+
+ visit admin_root_path
+ end
+
+ it 'displays callout' do
+ expect(page).to have_content 'Sign up for the GitLab Security Newsletter to get notified for security updates.'
+ expect(page).to have_link 'Sign up for the GitLab newsletter', href: 'https://about.gitlab.com/company/preference-center/'
+ end
+
+ context 'when link is clicked' do
+ before do
+ find_link('Sign up for the GitLab newsletter').click
+
+ visit admin_root_path
+ end
+
+ it_behaves_like 'hidden callout'
+ end
+
+ context 'when callout is dismissed' do
+ before do
+ find('[data-testid="close-security-newsletter-callout"]').click
+
+ visit admin_root_path
+ end
+
+ it_behaves_like 'hidden callout'
+ end
+ end
+
+ context 'when a non-admin is logged in' do
+ before do
+ sign_in(non_admin)
+ visit admin_root_path
+ end
+
+ it_behaves_like 'hidden callout'
+ end
+end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 275a87ca391..d2bde320c54 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js do
click_button "Check out branch"
- expect(page).to have_content 'git checkout -b "orphaned-branch" "origin/orphaned-branch"'
+ expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\''
end
it 'allows filtering multiple dropdowns' do
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index c9059395377..893dd2c76e0 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -78,40 +78,80 @@ RSpec.describe 'Profile > Password' do
end
end
- context 'Change passowrd' do
+ context 'Change password' do
+ let(:new_password) { '22233344' }
+
before do
sign_in(user)
visit(edit_profile_password_path)
end
- it 'does not change user passowrd without old one' do
- page.within '.update-password' do
- fill_passwords('22233344', '22233344')
+ shared_examples 'user enters an incorrect current password' do
+ subject do
+ page.within '.update-password' do
+ fill_in 'user_current_password', with: user_current_password
+ fill_passwords(new_password, new_password)
+ end
end
- page.within '.flash-container' do
- expect(page).to have_content 'You must provide a valid current password'
- end
- end
+ it 'handles the invalid password attempt, and prompts the user to try again', :aggregate_failures do
+ expect(Gitlab::AppLogger).to receive(:info)
+ .with(message: 'Invalid current password when attempting to update user password', username: user.username, ip: user.current_sign_in_ip)
+
+ subject
+
+ user.reload
- it 'does not change password with invalid old password' do
- page.within '.update-password' do
- fill_in 'user_current_password', with: 'invalid'
- fill_passwords('password', 'confirmation')
+ expect(user.failed_attempts).to eq(1)
+ expect(user.valid_password?(new_password)).to eq(false)
+ expect(current_path).to eq(edit_profile_password_path)
+
+ page.within '.flash-container' do
+ expect(page).to have_content('You must provide a valid current password')
+ end
end
- page.within '.flash-container' do
- expect(page).to have_content 'You must provide a valid current password'
+ it 'locks the user account when user passes the maximum attempts threshold', :aggregate_failures do
+ user.update!(failed_attempts: User.maximum_attempts.pred)
+
+ subject
+
+ expect(current_path).to eq(new_user_session_path)
+
+ page.within '.flash-container' do
+ expect(page).to have_content('Your account is locked.')
+ end
end
end
- it 'changes user password' do
- page.within '.update-password' do
- fill_in "user_current_password", with: user.password
- fill_passwords('22233344', '22233344')
+ context 'when current password is blank' do
+ let(:user_current_password) { nil }
+
+ it_behaves_like 'user enters an incorrect current password'
+ end
+
+ context 'when current password is incorrect' do
+ let(:user_current_password) {'invalid' }
+
+ it_behaves_like 'user enters an incorrect current password'
+ end
+
+ context 'when the password reset is successful' do
+ subject do
+ page.within '.update-password' do
+ fill_in "user_current_password", with: user.password
+ fill_passwords(new_password, new_password)
+ end
end
- expect(current_path).to eq new_user_session_path
+ it 'changes the password, logs the user out and prompts them to sign in again', :aggregate_failures do
+ expect { subject }.to change { user.reload.valid_password?(new_password) }.to(true)
+ expect(current_path).to eq new_user_session_path
+
+ page.within '.flash-container' do
+ expect(page).to have_content('Password was successfully updated. Please sign in again.')
+ end
+ end
end
end
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
new file mode 100644
index 00000000000..e1feca5031a
--- /dev/null
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Two factor auths' do
+ context 'when signed in' do
+ before do
+ allow(Gitlab).to receive(:com?) { true }
+ end
+
+ context 'when user has two-factor authentication disabled' do
+ let(:user) { create(:user ) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'requires the current password to set up two factor authentication', :js do
+ visit profile_two_factor_auth_path
+
+ register_2fa(user.reload.current_otp, '123')
+
+ expect(page).to have_content('You must provide a valid current password')
+
+ register_2fa(user.reload.current_otp, user.password)
+
+ expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.')
+
+ click_button 'Copy codes'
+ click_link 'Proceed'
+
+ expect(page).to have_content('Status: Enabled')
+ end
+ end
+
+ context 'when user has two-factor authentication enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'requires the current_password to disable two-factor authentication', :js do
+ visit profile_two_factor_auth_path
+
+ fill_in 'current_password', with: '123'
+
+ click_button 'Disable two-factor authentication'
+
+ page.accept_alert
+
+ expect(page).to have_content('You must provide a valid current password')
+
+ fill_in 'current_password', with: user.password
+
+ click_button 'Disable two-factor authentication'
+
+ page.accept_alert
+
+ expect(page).to have_content('Two-factor authentication has been disabled successfully!')
+ expect(page).to have_content('Enable two-factor authentication')
+ end
+
+ it 'requires the current_password to regernate recovery codes', :js do
+ visit profile_two_factor_auth_path
+
+ fill_in 'current_password', with: '123'
+
+ click_button 'Regenerate recovery codes'
+
+ expect(page).to have_content('You must provide a valid current password')
+
+ fill_in 'current_password', with: user.password
+
+ click_button 'Regenerate recovery codes'
+
+ expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.')
+ end
+ end
+
+ def register_2fa(pin, password)
+ fill_in 'pin_code', with: pin
+ fill_in 'current_password', with: password
+
+ click_button 'Register with two-factor app'
+ end
+ end
+end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 2c88860aef2..d9c919dae3d 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -807,6 +807,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
expect(current_path).to eq(profile_two_factor_auth_path)
fill_in 'pin_code', with: user.reload.current_otp
+ fill_in 'current_password', with: user.password
click_button 'Register with two-factor app'
click_button 'Copy codes'
diff --git a/spec/fixtures/api/schemas/public_api/v4/service.json b/spec/fixtures/api/schemas/public_api/v4/integration.json
index b6f13d1cfe7..b6f13d1cfe7 100644
--- a/spec/fixtures/api/schemas/public_api/v4/service.json
+++ b/spec/fixtures/api/schemas/public_api/v4/integration.json
diff --git a/spec/fixtures/api/schemas/public_api/v4/integrations.json b/spec/fixtures/api/schemas/public_api/v4/integrations.json
new file mode 100644
index 00000000000..e7ebe7652c9
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/integrations.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "integration.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/services.json b/spec/fixtures/api/schemas/public_api/v4/services.json
deleted file mode 100644
index 78c59ecfa10..00000000000
--- a/spec/fixtures/api/schemas/public_api/v4/services.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "array",
- "items": { "$ref": "service.json" }
-}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index e3aeace6383..1072e63b20b 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -7579,23 +7579,6 @@
}
}
],
- "triggers": [
- {
- "id": 123,
- "token": "cdbfasdf44a5958c83654733449e585",
- "project_id": 5,
- "owner_id": 1,
- "created_at": "2017-01-16T15:25:28.637Z",
- "updated_at": "2017-01-16T15:25:28.637Z"
- },
- {
- "id": 456,
- "token": "33a66349b5ad01fc00174af87804e40",
- "project_id": 5,
- "created_at": "2017-01-16T15:25:29.637Z",
- "updated_at": "2017-01-16T15:25:29.637Z"
- }
- ],
"pipeline_schedules": [
{
"id": 1,
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
index 93619f4fb44..2b5bda687b8 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
@@ -1,2 +1,2 @@
{"id":123,"token":"cdbfasdf44a5958c83654733449e585","project_id":5,"owner_id":1,"created_at":"2017-01-16T15:25:28.637Z","updated_at":"2017-01-16T15:25:28.637Z"}
-{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"}
+{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"} \ No newline at end of file
diff --git a/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap
new file mode 100644
index 00000000000..3fe0e570a54
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap
@@ -0,0 +1,99 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManageTwoFactorForm Disable button renders the component correctly 1`] = `
+VueWrapper {
+ "_emitted": Object {},
+ "_emittedByOrder": Array [],
+ "isFunctionalComponent": undefined,
+}
+`;
+
+exports[`ManageTwoFactorForm Disable button renders the component correctly 2`] = `
+<form
+ action="#"
+ class="gl-display-inline-block"
+ method="post"
+>
+ <input
+ data-testid="test-2fa-method-field"
+ name="_method"
+ type="hidden"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ />
+
+ <div
+ class="form-group gl-form-group"
+ id="__BVID__15"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="current-password"
+ id="__BVID__15__BV_label_"
+ >
+ Current password
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <input
+ aria-required="true"
+ class="gl-form-input form-control"
+ data-qa-selector="current_password_field"
+ id="current-password"
+ name="current_password"
+ required="required"
+ type="password"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <button
+ class="btn btn-danger gl-mr-3 gl-display-inline-block btn-danger btn-md gl-button"
+ data-confirm="Are you sure? This will invalidate your registered applications and U2F devices."
+ data-form-action="2fa_auth_path"
+ data-form-method="2fa_auth_method"
+ data-testid="test-2fa-disable-button"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Disable two-factor authentication
+
+ </span>
+ </button>
+
+ <button
+ class="btn gl-display-inline-block btn-default btn-md gl-button"
+ data-form-action="2fa_codes_path"
+ data-form-method="2fa_codes_method"
+ data-testid="test-2fa-regenerate-codes-button"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Regenerate recovery codes
+
+ </span>
+ </button>
+</form>
+`;
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
new file mode 100644
index 00000000000..384579c6876
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -0,0 +1,98 @@
+import { within } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ManageTwoFactorForm, {
+ i18n,
+} from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue';
+
+describe('ManageTwoFactorForm', () => {
+ let wrapper;
+
+ const createComponent = (options = {}) => {
+ wrapper = extendedWrapper(
+ mount(ManageTwoFactorForm, {
+ provide: {
+ webauthnEnabled: options?.webauthnEnabled || false,
+ profileTwoFactorAuthPath: '2fa_auth_path',
+ profileTwoFactorAuthMethod: '2fa_auth_method',
+ codesProfileTwoFactorAuthPath: '2fa_codes_path',
+ codesProfileTwoFactorAuthMethod: '2fa_codes_method',
+ },
+ }),
+ );
+ };
+
+ const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
+ const queryByLabelText = (text, options) =>
+ within(wrapper.element).queryByLabelText(text, options);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Current password field', () => {
+ it('renders the current password field', () => {
+ expect(queryByLabelText(i18n.currentPassword).tagName).toEqual('INPUT');
+ });
+ });
+
+ describe('Disable button', () => {
+ it('renders the component correctly', () => {
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has the right confirm text', () => {
+ expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual(
+ i18n.confirm,
+ );
+ });
+
+ describe('when webauthnEnabled', () => {
+ beforeEach(() => {
+ createComponent({
+ webauthnEnabled: true,
+ });
+ });
+
+ it('has the right confirm text', () => {
+ expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual(
+ i18n.confirmWebAuthn,
+ );
+ });
+ });
+
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = wrapper.find('form');
+ const disableButton = wrapper.findByTestId('test-2fa-disable-button').element;
+ const methodInput = wrapper.findByTestId('test-2fa-method-field').element;
+
+ form.trigger('submit', { submitter: disableButton });
+
+ await wrapper.vm.$nextTick();
+
+ expect(form.element.getAttribute('action')).toEqual('2fa_auth_path');
+ expect(methodInput.getAttribute('value')).toEqual('2fa_auth_method');
+ });
+ });
+
+ describe('Regenerate recovery codes button', () => {
+ it('renders the button', () => {
+ expect(queryByText(i18n.regenerateRecoveryCodes)).toEqual(expect.any(HTMLElement));
+ });
+
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = wrapper.find('form');
+ const regenerateCodesButton = wrapper.findByTestId('test-2fa-regenerate-codes-button')
+ .element;
+ const methodInput = wrapper.findByTestId('test-2fa-method-field').element;
+
+ form.trigger('submit', { submitter: regenerateCodesButton });
+
+ await wrapper.vm.$nextTick();
+
+ expect(form.element.getAttribute('action')).toEqual('2fa_codes_path');
+ expect(methodInput.getAttribute('value')).toEqual('2fa_codes_method');
+ });
+ });
+});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 211ed064762..94ad7759110 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -574,6 +574,15 @@ describe('GfmAutoComplete', () => {
}),
).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
});
+
+ it('escapes title in the template as it is user input', () => {
+ expect(
+ GfmAutoComplete.Issues.templateFunction({
+ id: 5,
+ title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
+ }),
+ ).toBe('<li><small>5</small> &dollar;{search}&lt;script&gt;oh no &dollar;</li>');
+ });
});
describe('GfmAutoComplete.Members', () => {
@@ -608,16 +617,18 @@ describe('GfmAutoComplete', () => {
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
- it('should add escaped title if title is set', () => {
+ it('escapes title in the template as it is user input', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
- title: 'MyGroup+',
+ title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
- ).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
+ ).toBe(
+ '<li>IMG my-group <small>&dollar;{search}&lt;script&gt;oh no &dollar;</small> <i class="icon"/></li>',
+ );
});
it('should add user availability status if availabilityStatus is set', () => {
@@ -782,6 +793,15 @@ describe('GfmAutoComplete', () => {
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
});
+
+ it('escapes title in the template as it is user input', () => {
+ const color = '#123456';
+ const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
+
+ expect(GfmAutoComplete.Labels.templateFunction(color, title)).toBe(
+ '<li><span class="dropdown-label-box" style="background: #123456"></span> &dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ );
+ });
});
describe('emoji', () => {
@@ -829,4 +849,15 @@ describe('GfmAutoComplete', () => {
});
});
});
+
+ describe('milestones', () => {
+ it('escapes title in the template as it is user input', () => {
+ const expired = false;
+ const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
+
+ expect(GfmAutoComplete.Milestones.templateFunction(title, expired)).toBe(
+ '<li>&dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ );
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 1f3659b5c76..9570d2a831c 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -363,4 +363,25 @@ describe('text_utility', () => {
expect(textUtils.insertFinalNewline(input, '\r\n')).toBe(output);
});
});
+
+ describe('escapeShellString', () => {
+ it.each`
+ character | input | output
+ ${'"'} | ${'";echo "you_shouldnt_run_this'} | ${'\'";echo "you_shouldnt_run_this\''}
+ ${'$'} | ${'$IFS'} | ${"'$IFS'"}
+ ${'\\'} | ${'evil-branch-name\\'} | ${"'evil-branch-name\\'"}
+ ${'!'} | ${'!event'} | ${"'!event'"}
+ `(
+ 'should not escape the $character character but wrap in single-quotes',
+ ({ input, output }) => {
+ expect(textUtils.escapeShellString(input)).toBe(output);
+ },
+ );
+
+ it("should escape the ' character and wrap in single-quotes", () => {
+ expect(textUtils.escapeShellString("fix-'bug-behavior'")).toBe(
+ "'fix-'\\''bug-behavior'\\'''",
+ );
+ });
+ });
});
diff --git a/spec/frontend/users_select/index_spec.js b/spec/frontend/users_select/index_spec.js
index 99caaf61c54..0d2aae78944 100644
--- a/spec/frontend/users_select/index_spec.js
+++ b/spec/frontend/users_select/index_spec.js
@@ -1,3 +1,5 @@
+import { escape } from 'lodash';
+import UsersSelect from '~/users_select/index';
import {
createInputsModelExpectation,
createUnassignedExpectation,
@@ -91,5 +93,19 @@ describe('~/users_select/index', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
});
+
+ describe('renderApprovalRules', () => {
+ const ruleNames = ['simple-name', '"\'<>&', '"><script>alert(1)<script>'];
+
+ it.each(ruleNames)('escapes rule name correctly for %s', (name) => {
+ const escapedName = escape(name);
+
+ expect(
+ UsersSelect.prototype.renderApprovalRules('reviewer', [{ name }]),
+ ).toMatchInterpolatedText(
+ `<div class="gl-display-flex gl-font-sm"> <span class="gl-text-truncate" title="${escapedName}">${escapedName}</span> </div>`,
+ );
+ });
+ });
});
});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
index bd22183cbea..913d5860b48 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
@@ -8,11 +8,9 @@ describe('MRWidgetHowToMerge', () => {
function mountComponent({ data = {}, props = {} } = {}) {
wrapper = shallowMount(MrWidgetHowToMergeModal, {
data() {
- return { ...data };
- },
- propsData: {
- ...props,
+ return data;
},
+ propsData: props,
stubs: {},
});
}
@@ -57,4 +55,16 @@ describe('MRWidgetHowToMerge', () => {
mountComponent({ props: { isFork: true } });
expect(findInstructionsFields().at(0).text()).toContain('FETCH_HEAD');
});
+
+ it('escapes the target branch name shell-secure', () => {
+ mountComponent({ props: { targetBranch: '";echo$IFS"you_shouldnt_run_this' } });
+
+ expect(findInstructionsFields().at(1).text()).toContain('\'";echo$IFS"you_shouldnt_run_this\'');
+ });
+
+ it('escapes the source branch name shell-secure', () => {
+ mountComponent({ props: { sourceBranch: 'branch-of-$USER' } });
+
+ expect(findInstructionsFields().at(0).text()).toContain("'branch-of-$USER'");
+ });
});
diff --git a/spec/frontend_integration/fixture_generators.yml b/spec/frontend_integration/fixture_generators.yml
new file mode 100644
index 00000000000..1f6ff85352d
--- /dev/null
+++ b/spec/frontend_integration/fixture_generators.yml
@@ -0,0 +1,5 @@
+- spec/frontend/fixtures/api_projects.rb
+- spec/frontend/fixtures/api_merge_requests.rb
+- spec/frontend/fixtures/projects_json.rb
+- spec/frontend/fixtures/merge_requests_diffs.rb
+- spec/frontend/fixtures/raw.rb
diff --git a/spec/graphql/types/group_invitation_type_spec.rb b/spec/graphql/types/group_invitation_type_spec.rb
index dab2d43fc90..9eedc2db81d 100644
--- a/spec/graphql/types/group_invitation_type_spec.rb
+++ b/spec/graphql/types/group_invitation_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Types::GroupInvitationType do
specify { expect(described_class.graphql_name).to eq('GroupInvitation') }
- specify { expect(described_class).to require_graphql_authorizations(:read_group) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_group) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/project_invitation_type_spec.rb b/spec/graphql/types/project_invitation_type_spec.rb
index 148a763a5fa..5c0b03c2505 100644
--- a/spec/graphql/types/project_invitation_type_spec.rb
+++ b/spec/graphql/types/project_invitation_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Types::ProjectInvitationType do
specify { expect(described_class.graphql_name).to eq('ProjectInvitation') }
- specify { expect(described_class).to require_graphql_authorizations(:read_project) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/helpers/external_link_helper_spec.rb b/spec/helpers/external_link_helper_spec.rb
index f5bb0568824..b746cb04ab3 100644
--- a/spec/helpers/external_link_helper_spec.rb
+++ b/spec/helpers/external_link_helper_spec.rb
@@ -13,8 +13,14 @@ RSpec.describe ExternalLinkHelper do
it 'allows options when creating external link with icon' do
link = external_link('https://gitlab.com', 'https://gitlab.com', { "data-foo": "bar", class: "externalLink" }).to_s
-
expect(link).to start_with('<a target="_blank" rel="noopener noreferrer" data-foo="bar" class="externalLink" href="https://gitlab.com">https://gitlab.com')
expect(link).to include('data-testid="external-link-icon"')
end
+
+ it 'sanitizes and returns external link with icon' do
+ link = external_link('sanitized link content', 'javascript:alert()').to_s
+ expect(link).not_to include('href="javascript:alert()"')
+ expect(link).to start_with('<a target="_blank" rel="noopener noreferrer">sanitized link content')
+ expect(link).to include('data-testid="external-link-icon"')
+ end
end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 4784d0aff26..af2957d72c7 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -35,22 +35,22 @@ RSpec.describe IconsHelper do
it 'returns svg icon html with DEFAULT_ICON_SIZE' do
expect(sprite_icon(icon_name).to_s)
- .to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html without size class' do
expect(sprite_icon(icon_name, size: nil).to_s)
- .to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes' do
expect(sprite_icon(icon_name, size: 72).to_s)
- .to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes + additional class' do
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
- .to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
describe 'non existing icon' do
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index 794ff5ee945..f738ba855b8 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -293,4 +293,37 @@ RSpec.describe UserCalloutsHelper do
it { is_expected.to eq(false) }
end
end
+
+ describe '.show_security_newsletter_user_callout?' do
+ let_it_be(:admin) { create(:user, :admin) }
+
+ subject { helper.show_security_newsletter_user_callout? }
+
+ context 'when `current_user` is not an admin' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:user_dismissed?).with(described_class::SECURITY_NEWSLETTER_CALLOUT) { false }
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when user has dismissed callout' do
+ before do
+ allow(helper).to receive(:current_user).and_return(admin)
+ allow(helper).to receive(:user_dismissed?).with(described_class::SECURITY_NEWSLETTER_CALLOUT) { true }
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when `current_user` is an admin and user has not dismissed callout' do
+ before do
+ allow(helper).to receive(:current_user).and_return(admin)
+ allow(helper).to receive(:user_dismissed?).with(described_class::SECURITY_NEWSLETTER_CALLOUT) { false }
+ end
+
+ it { is_expected.to be true }
+ end
+ end
end
diff --git a/spec/initializers/100_patch_omniauth_oauth2_spec.rb b/spec/initializers/100_patch_omniauth_oauth2_spec.rb
new file mode 100644
index 00000000000..0c436e4ef45
--- /dev/null
+++ b/spec/initializers/100_patch_omniauth_oauth2_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'OmniAuth::Strategies::OAuth2', type: :strategy do
+ let(:strategy) { [OmniAuth::Strategies::OAuth2] }
+
+ it 'verifies the gem version' do
+ current_version = OmniAuth::OAuth2::VERSION
+ expected_version = '1.7.1'
+
+ expect(current_version).to eq(expected_version), <<~EOF
+ New version #{current_version} of the `omniauth-oauth2` gem detected!
+
+ Please check if the monkey patches in `config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb`
+ are still needed, and either update/remove them, or bump the version in this spec.
+
+ EOF
+ end
+
+ context 'when a custom error message is passed from an OAuth2 provider' do
+ let(:message) { 'Please go to https://evil.com' }
+ let(:state) { 'secret' }
+ let(:callback_path) { '/users/auth/oauth2/callback' }
+ let(:params) { { state: state, error: 'evil_key', error_description: message } }
+ let(:error) { last_request.env['omniauth.error'] }
+
+ before do
+ env('rack.session', { 'omniauth.state' => state })
+ end
+
+ it 'returns the custom error message if the state is valid' do
+ get callback_path, **params
+
+ expect(error.message).to eq("evil_key | #{message}")
+ end
+
+ it 'returns the custom `error_reason` message if the `error_description` is blank' do
+ get callback_path, **params.merge(error_description: ' ', error_reason: 'custom reason')
+
+ expect(error.message).to eq('evil_key | custom reason')
+ end
+
+ it 'returns a CSRF error if the state is invalid' do
+ get callback_path, **params.merge(state: 'invalid')
+
+ expect(error.message).to eq('csrf_detected | CSRF detected')
+ end
+
+ it 'returns a CSRF error if the state is missing' do
+ get callback_path, **params.without(:state)
+
+ expect(error.message).to eq('csrf_detected | CSRF detected')
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
index 2c64657d69d..820ebeb6945 100644
--- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
@@ -63,6 +63,16 @@ RSpec.describe Banzai::Filter::SpacedLinkFilter do
end
end
+ it 'does not process malicious input' do
+ Timeout.timeout(10) do
+ doc = filter('[ (](' * 60_000)
+
+ found_links = doc.css('a')
+
+ expect(found_links.size).to eq(0)
+ end
+ end
+
it 'converts multiple URLs' do
link1 = '[first](slug one)'
link2 = '[second](http://example.com/slug two)'
diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
index 57a258b0d9f..7d156c2c3df 100644
--- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
@@ -186,4 +186,20 @@ RSpec.describe BulkImports::NdjsonPipeline do
end
end
end
+
+ describe '#relation_factory' do
+ context 'when portable is group' do
+ it 'returns group relation factory' do
+ expect(subject.relation_factory).to eq(Gitlab::ImportExport::Group::RelationFactory)
+ end
+ end
+
+ context 'when portable is project' do
+ subject { NdjsonPipelineClass.new(project, user) }
+
+ it 'returns project relation factory' do
+ expect(subject.relation_factory).to eq(Gitlab::ImportExport::Project::RelationFactory)
+ end
+ end
+ end
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
new file mode 100644
index 00000000000..97fcddefd42
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Project',
+ destination_namespace: group.full_path
+ )
+ end
+
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ let(:issue_attributes) { {} }
+ let(:issue) do
+ {
+ 'iid' => 7,
+ 'title' => 'Imported Issue',
+ 'description' => 'Description',
+ 'state' => 'opened',
+ 'updated_at' => '2016-06-14T15:02:47.967Z',
+ 'author_id' => 22
+ }.merge(issue_attributes)
+ end
+
+ subject(:pipeline) { described_class.new(context) }
+
+ describe '#run' do
+ before do
+ group.add_owner(user)
+ issue_with_index = [issue, 0]
+
+ allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [issue_with_index]))
+ end
+
+ pipeline.run
+ end
+
+ it 'imports issue into destination project' do
+ expect(project.issues.count).to eq(1)
+
+ imported_issue = project.issues.last
+
+ aggregate_failures do
+ expect(imported_issue.iid).to eq(7)
+ expect(imported_issue.title).to eq(issue['title'])
+ expect(imported_issue.description).to eq(issue['description'])
+ expect(imported_issue.author).to eq(user)
+ expect(imported_issue.state).to eq('opened')
+ expect(imported_issue.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
+ end
+ end
+
+ context 'zoom meetings' do
+ let(:issue_attributes) { { 'zoom_meetings' => [{ 'url' => 'https://zoom.us/j/123456789' }] } }
+
+ it 'restores zoom meetings' do
+ expect(project.issues.last.zoom_meetings.first.url).to eq('https://zoom.us/j/123456789')
+ end
+ end
+
+ context 'sentry issue' do
+ let(:issue_attributes) { { 'sentry_issue' => { 'sentry_issue_identifier' => '1234567891' } } }
+
+ it 'restores sentry issue information' do
+ expect(project.issues.last.sentry_issue.sentry_issue_identifier).to eq(1234567891)
+ end
+ end
+
+ context 'award emoji' do
+ let(:issue_attributes) { { 'award_emoji' => [{ 'name' => 'musical_keyboard', 'user_id' => 22 }] } }
+
+ it 'has award emoji on an issue' do
+ award_emoji = project.issues.last.award_emoji.first
+
+ expect(award_emoji.name).to eq('musical_keyboard')
+ expect(award_emoji.user).to eq(user)
+ end
+ end
+ context 'issue state' do
+ let(:issue_attributes) { { 'state' => 'closed' } }
+
+ it 'restores issue state' do
+ expect(project.issues.last.state).to eq('closed')
+ end
+ end
+
+ context 'labels' do
+ let(:issue_attributes) do
+ {
+ 'label_links' => [
+ { 'label' => { 'title' => 'imported label 1', 'type' => 'ProjectLabel' } },
+ { 'label' => { 'title' => 'imported label 2', 'type' => 'ProjectLabel' } }
+ ]
+ }
+ end
+
+ it 'restores issue labels' do
+ expect(project.issues.last.labels.pluck(:title)).to contain_exactly('imported label 1', 'imported label 2')
+ end
+ end
+
+ context 'milestone' do
+ let(:issue_attributes) { { 'milestone' => { 'title' => 'imported milestone' } } }
+
+ it 'restores issue milestone' do
+ expect(project.issues.last.milestone.title).to eq('imported milestone')
+ end
+ end
+
+ context 'timelogs' do
+ let(:issue_attributes) { { 'timelogs' => [{ 'time_spent' => 72000, 'spent_at' => '2019-12-27T00:00:00.000Z', 'user_id' => 22 }] } }
+
+ it 'restores issue timelogs' do
+ timelog = project.issues.last.timelogs.first
+
+ aggregate_failures do
+ expect(timelog.time_spent).to eq(72000)
+ expect(timelog.spent_at).to eq("2019-12-27T00:00:00.000Z")
+ end
+ end
+ end
+
+ context 'notes' do
+ let(:issue_attributes) do
+ {
+ 'notes' => [
+ {
+ 'note' => 'Issue note',
+ 'author_id' => 22,
+ 'author' => {
+ 'name' => 'User 22'
+ },
+ 'updated_at' => '2016-06-14T15:02:47.770Z',
+ 'award_emoji' => [
+ {
+ 'name' => 'clapper',
+ 'user_id' => 22
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ it 'restores issue notes and their award emoji' do
+ note = project.issues.last.notes.first
+
+ aggregate_failures do
+ expect(note.note).to eq("Issue note\n\n *By User 22 on 2016-06-14T15:02:47 (imported from GitLab)*")
+ expect(note.award_emoji.first.name).to eq('clapper')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb
index d74f72cee03..0109b65427f 100644
--- a/spec/lib/bulk_imports/projects/stage_spec.rb
+++ b/spec/lib/bulk_imports/projects/stage_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe BulkImports::Projects::Stage do
[
[0, BulkImports::Projects::Pipelines::ProjectPipeline],
[1, BulkImports::Common::Pipelines::LabelsPipeline],
- [2, BulkImports::Common::Pipelines::EntityFinisher]
+ [2, BulkImports::Projects::Pipelines::IssuesPipeline],
+ [3, BulkImports::Common::Pipelines::EntityFinisher]
]
end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index 28e93a8da52..2543eb3a5e9 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -81,32 +81,72 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
expect(subject.find_sessionless_user(:api)).to eq job_token_user
end
- it 'returns lfs_token user if no job_token user found' do
- allow_any_instance_of(described_class)
- .to receive(:find_user_from_lfs_token)
- .and_return(lfs_token_user)
-
- expect(subject.find_sessionless_user(:api)).to eq lfs_token_user
- end
-
- it 'returns basic_auth_access_token user if no lfs_token user found' do
+ it 'returns nil even if basic_auth_access_token is available' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_personal_access_token)
.and_return(basic_auth_access_token_user)
- expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
+ expect(subject.find_sessionless_user(:api)).to be_nil
end
- it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
+ it 'returns nil even if find_user_from_lfs_token is available' do
allow_any_instance_of(described_class)
- .to receive(:find_user_from_basic_auth_password)
- .and_return(basic_auth_password_user)
+ .to receive(:find_user_from_lfs_token)
+ .and_return(lfs_token_user)
- expect(subject.find_sessionless_user(:api)).to eq basic_auth_password_user
+ expect(subject.find_sessionless_user(:api)).to be_nil
end
it 'returns nil if no user found' do
- expect(subject.find_sessionless_user(:api)).to be_blank
+ expect(subject.find_sessionless_user(:api)).to be_nil
+ end
+
+ context 'in an API request' do
+ before do
+ env['SCRIPT_NAME'] = '/api/v4/projects'
+ end
+
+ it 'returns basic_auth_access_token user if no job_token_user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_personal_access_token)
+ .and_return(basic_auth_access_token_user)
+
+ expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
+ end
+ end
+
+ context 'in a Git request' do
+ before do
+ env['SCRIPT_NAME'] = '/group/project.git/info/refs'
+ end
+
+ it 'returns lfs_token user if no job_token user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_lfs_token)
+ .and_return(lfs_token_user)
+
+ expect(subject.find_sessionless_user(nil)).to eq lfs_token_user
+ end
+
+ it 'returns basic_auth_access_token user if no lfs_token user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_personal_access_token)
+ .and_return(basic_auth_access_token_user)
+
+ expect(subject.find_sessionless_user(nil)).to eq basic_auth_access_token_user
+ end
+
+ it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
+ allow_any_instance_of(described_class)
+ .to receive(:find_user_from_basic_auth_password)
+ .and_return(basic_auth_password_user)
+
+ expect(subject.find_sessionless_user(nil)).to eq basic_auth_password_user
+ end
+
+ it 'returns nil if no user found' do
+ expect(subject.find_sessionless_user(nil)).to be_blank
+ end
end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
diff --git a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb
index f906870195a..876c23a91bd 100644
--- a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb
+++ b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb
@@ -3,33 +3,50 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do
- let(:user) { create(:user) }
+ using RSpec::Parameterized::TableSyntax
- subject { described_class.new(user) }
+ subject(:verifier) { described_class.new(user) }
- describe '#two_factor_authentication_required?' do
- describe 'when it is required on application level' do
- it 'returns true' do
- stub_application_setting require_two_factor_authentication: true
+ let(:user) { build_stubbed(:user, otp_grace_period_started_at: Time.zone.now) }
- expect(subject.two_factor_authentication_required?).to be_truthy
- end
- end
+ describe '#two_factor_authentication_enforced?' do
+ subject { verifier.two_factor_authentication_enforced? }
- describe 'when it is required on group level' do
- it 'returns true' do
- allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(true)
+ where(:instance_level_enabled, :group_level_enabled, :grace_period_expired, :should_be_enforced) do
+ false | false | true | false
+ true | false | false | false
+ true | false | true | true
+ false | true | false | false
+ false | true | true | true
+ end
- expect(subject.two_factor_authentication_required?).to be_truthy
+ with_them do
+ before do
+ stub_application_setting(require_two_factor_authentication: instance_level_enabled)
+ allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled)
+ stub_application_setting(two_factor_grace_period: grace_period_expired ? 0 : 1.month.in_hours)
end
+
+ it { is_expected.to eq(should_be_enforced) }
end
+ end
- describe 'when it is not required' do
- it 'returns false when not required on group level' do
- allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(false)
+ describe '#two_factor_authentication_required?' do
+ subject { verifier.two_factor_authentication_required? }
+
+ where(:instance_level_enabled, :group_level_enabled, :should_be_required) do
+ true | false | true
+ false | true | true
+ false | false | false
+ end
- expect(subject.two_factor_authentication_required?).to be_falsey
+ with_them do
+ before do
+ stub_application_setting(require_two_factor_authentication: instance_level_enabled)
+ allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled)
end
+
+ it { is_expected.to eq(should_be_required) }
end
end
@@ -85,25 +102,21 @@ RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do
end
describe '#two_factor_grace_period_expired?' do
- before do
- allow(user).to receive(:otp_grace_period_started_at).and_return(4.hours.ago)
- end
-
it 'returns true if the grace period has expired' do
- allow(subject).to receive(:two_factor_grace_period).and_return(2)
+ stub_application_setting two_factor_grace_period: 0
expect(subject.two_factor_grace_period_expired?).to be_truthy
end
it 'returns false if the grace period has not expired' do
- allow(subject).to receive(:two_factor_grace_period).and_return(6)
+ stub_application_setting two_factor_grace_period: 1.month.in_hours
expect(subject.two_factor_grace_period_expired?).to be_falsey
end
context 'when otp_grace_period_started_at is nil' do
it 'returns false' do
- allow(user).to receive(:otp_grace_period_started_at).and_return(nil)
+ user.otp_grace_period_started_at = nil
expect(subject.two_factor_grace_period_expired?).to be_falsey
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index cc592bb8f24..5ec6e23774a 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -386,7 +386,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
shared_examples 'with an invalid access token' do
it 'fails for a non-member' do
expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
- .to have_attributes(auth_failure )
+ .to have_attributes(auth_failure)
end
context 'when project bot user is blocked' do
@@ -396,7 +396,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails for a blocked project bot' do
expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
- .to have_attributes(auth_failure )
+ .to have_attributes(auth_failure)
end
end
end
@@ -466,6 +466,41 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
.to have_attributes(auth_failure)
end
+ context 'when 2fa is enabled globally' do
+ let_it_be(:user) do
+ create(:user, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
+ end
+
+ before do
+ stub_application_setting(require_two_factor_authentication: true)
+ end
+
+ it 'fails if grace period expired' do
+ stub_application_setting(two_factor_grace_period: 0)
+
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ .to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
+ end
+
+ it 'goes through if grace period is not expired yet' do
+ stub_application_setting(two_factor_grace_period: 72)
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
+ end
+ end
+
+ context 'when 2fa is enabled personally' do
+ let(:user) do
+ create(:user, :two_factor, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
+ end
+
+ it 'fails' do
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ .to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
+ end
+ end
+
it 'goes through lfs authentication' do
user = create(
:user,
@@ -757,16 +792,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
describe 'find_with_user_password' do
let!(:user) do
create(:user,
- username: username,
- password: password,
- password_confirmation: password)
+ username: username,
+ password: password,
+ password_confirmation: password)
end
let(:username) { 'John' } # username isn't lowercase, test this
let(:password) { 'my-secret' }
it "finds user by valid login/password" do
- expect( gl_auth.find_with_user_password(username, password) ).to eql user
+ expect(gl_auth.find_with_user_password(username, password)).to eql user
end
it 'finds user by valid email/password with case-insensitive email' do
@@ -779,12 +814,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user with invalid password" do
password = 'wrong'
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it "does not find user with invalid login" do
user = 'wrong'
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
include_examples 'user login operation with unique ip limit' do
@@ -796,13 +831,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'finds the user in deactivated state' do
user.deactivate!
- expect( gl_auth.find_with_user_password(username, password) ).to eql user
+ expect(gl_auth.find_with_user_password(username, password)).to eql user
end
it "does not find user in blocked state" do
user.block
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it 'does not find user in locked state' do
@@ -814,13 +849,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user in ldap_blocked state" do
user.ldap_block
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
it 'does not find user in blocked_pending_approval state' do
user.block_pending_approval
- expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ expect(gl_auth.find_with_user_password(username, password)).not_to eql user
end
context 'with increment_failed_attempts' do
diff --git a/spec/lib/gitlab/fogbugz_import/importer_spec.rb b/spec/lib/gitlab/fogbugz_import/importer_spec.rb
index eb0c4da6ce3..9b58b772d1a 100644
--- a/spec/lib/gitlab/fogbugz_import/importer_spec.rb
+++ b/spec/lib/gitlab/fogbugz_import/importer_spec.rb
@@ -4,23 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::FogbugzImport::Importer do
let(:project) { create(:project_empty_repo) }
- let(:importer) { described_class.new(project) }
- let(:repo) do
- instance_double(Gitlab::FogbugzImport::Repository,
- safe_name: 'vim',
- path: 'vim',
- raw_data: '')
- end
-
- let(:import_data) { { 'repo' => repo } }
- let(:credentials) do
- {
- 'fb_session' => {
- 'uri' => 'https://testing.fogbugz.com',
- 'token' => 'token'
- }
- }
- end
+ let(:fogbugz_project) { { 'ixProject' => project.id, 'sProject' => 'vim' } }
+ let(:import_data) { { 'repo' => fogbugz_project } }
+ let(:base_url) { 'https://testing.fogbugz.com' }
+ let(:token) { 'token' }
+ let(:credentials) { { 'fb_session' => { 'uri' => base_url, 'token' => token } } }
let(:closed_bug) do
{
@@ -46,18 +34,22 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
let(:fogbugz_bugs) { [opened_bug, closed_bug] }
+ subject(:importer) { described_class.new(project) }
+
before do
project.create_import_data(data: import_data, credentials: credentials)
- allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listCategories).and_return([])
- allow_any_instance_of(Gitlab::FogbugzImport::Client).to receive(:cases).and_return(fogbugz_bugs)
+
+ stub_fogbugz('listProjects', projects: { project: [fogbugz_project], count: 1 })
+ stub_fogbugz('listCategories', categories: { category: [], count: 0 })
+ stub_fogbugz('search', cases: { case: fogbugz_bugs, count: fogbugz_bugs.size })
end
it 'imports bugs' do
- expect { importer.execute }.to change { Issue.count }.by(2)
+ expect { subject.execute }.to change { Issue.count }.by(2)
end
it 'imports opened bugs' do
- importer.execute
+ subject.execute
issue = Issue.where(project_id: project.id).find_by_title(opened_bug[:sTitle])
@@ -65,10 +57,54 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
end
it 'imports closed bugs' do
- importer.execute
+ subject.execute
issue = Issue.where(project_id: project.id).find_by_title(closed_bug[:sTitle])
expect(issue.state_id).to eq(Issue.available_states[:closed])
end
+
+ context 'verify url' do
+ context 'when host is localhost' do
+ let(:base_url) { 'https://localhost:3000' }
+
+ it 'does not allow localhost requests' do
+ expect { subject.execute }
+ .to raise_error(
+ ::Gitlab::HTTP::BlockedUrlError,
+ "URL 'https://localhost:3000/api.asp' is blocked: Requests to localhost are not allowed"
+ )
+ end
+ end
+
+ context 'when host is on local network' do
+ let(:base_url) { 'http://192.168.0.1' }
+
+ it 'does not allow localhost requests' do
+ expect { subject.execute }
+ .to raise_error(
+ ::Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://192.168.0.1/api.asp' is blocked: Requests to the local network are not allowed"
+ )
+ end
+ end
+
+ context 'when host is ftp protocol' do
+ let(:base_url) { 'ftp://testing' }
+
+ it 'only accept http and https requests' do
+ expect { subject.execute }
+ .to raise_error(
+ HTTParty::UnsupportedURIScheme,
+ "'ftp://testing/api.asp' Must be HTTP, HTTPS or Generic"
+ )
+ end
+ end
+ end
+
+ def stub_fogbugz(command, response)
+ stub_request(:post, "#{base_url}/api.asp")
+ .with(body: hash_including({ 'cmd' => command, 'token' => token }))
+ .to_return(status: 200, body: response.to_xml(root: :response))
+ end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index bf682e4e4c6..bf2e3c7f5f8 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -435,17 +435,19 @@ RSpec.describe Gitlab::GitAccess do
it 'disallows users with expired password to pull' do
project.add_maintainer(user)
- user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.minutes.ago)
expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
- it 'allows ldap users with expired password to pull' do
- project.add_maintainer(user)
- user.update!(password_expires_at: 2.minutes.ago)
- allow(user).to receive(:ldap_user?).and_return(true)
+ context 'with an ldap user' do
+ let(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
- expect { pull_access_check }.not_to raise_error
+ it 'allows ldap users with expired password to pull' do
+ project.add_maintainer(user)
+
+ expect { pull_access_check }.not_to raise_error
+ end
end
context 'when the project repository does not exist' do
@@ -987,24 +989,23 @@ RSpec.describe Gitlab::GitAccess do
end
it 'disallows users with expired password to push' do
- user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.minutes.ago)
expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
- it 'allows ldap users with expired password to push' do
- user.update!(password_expires_at: 2.minutes.ago)
- allow(user).to receive(:ldap_user?).and_return(true)
+ context 'with an ldap user' do
+ let(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
- expect { push_access_check }.not_to raise_error
- end
+ it 'allows ldap users with expired password to push' do
+ expect { push_access_check }.not_to raise_error
+ end
- it 'disallows blocked ldap users with expired password to push' do
- user.block
- user.update!(password_expires_at: 2.minutes.ago)
- allow(user).to receive(:ldap_user?).and_return(true)
+ it 'disallows blocked ldap users with expired password to push' do
+ user.block
- expect { push_access_check }.to raise_forbidden("Your account has been blocked.")
+ expect { push_access_check }.to raise_forbidden("Your account has been blocked.")
+ end
end
it 'cleans up the files' do
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
index 69828c143db..401ffee9c28 100644
--- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::Redis::TraceChunksCheck,
+ Gitlab::HealthChecks::Redis::RateLimitingCheck,
Gitlab::HealthChecks::GitalyCheck
]
end
diff --git a/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb b/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb
new file mode 100644
index 00000000000..1521fc99cde
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../simple_check_shared'
+
+RSpec.describe Gitlab::HealthChecks::Redis::RateLimitingCheck do
+ include_examples 'simple_check', 'redis_rate_limiting_ping', 'RedisRateLimiting', 'PONG'
+end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 82f465c4f9e..518a9337826 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -445,8 +445,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
expect(@project.merge_requests.size).to eq(9)
end
- it 'only restores valid triggers' do
- expect(@project.triggers.size).to eq(1)
+ it 'does not restore triggers' do
+ expect(@project.triggers.size).to eq(0)
end
it 'has the correct number of pipelines and statuses' do
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index a9efa32f986..287be24d11f 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -401,15 +401,6 @@ Ci::Variable:
- encrypted_value
- encrypted_value_salt
- encrypted_value_iv
-Ci::Trigger:
-- id
-- token
-- project_id
-- created_at
-- updated_at
-- owner_id
-- description
-- ref
Ci::PipelineSchedule:
- id
- description
@@ -556,7 +547,6 @@ Project:
- disable_overriding_approvers_per_merge_request
- merge_requests_ff_only_enabled
- issues_template
-- repository_size_limit
- sync_time
- service_desk_enabled
- last_repository_updated_at
diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb
index ebc2e92a0dd..0da44dfb8d8 100644
--- a/spec/lib/gitlab/instrumentation/redis_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_spec.rb
@@ -76,7 +76,8 @@ RSpec.describe Gitlab::Instrumentation::Redis do
details_row.merge(storage: 'Cache'),
details_row.merge(storage: 'Queues'),
details_row.merge(storage: 'SharedState'),
- details_row.merge(storage: 'TraceChunks'))
+ details_row.merge(storage: 'TraceChunks'),
+ details_row.merge(storage: 'RateLimiting'))
end
end
end
diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb
index 0929b90d1f4..83ba5858d81 100644
--- a/spec/lib/gitlab/legacy_github_import/client_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb
@@ -86,6 +86,15 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do
it 'builds a endpoint with the given options' do
expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
end
+
+ context 'and hostname' do
+ subject(:client) { described_class.new(token, host: 'https://167.99.148.217/', api_version: 'v1', hostname: 'try.gitea.io') }
+
+ it 'builds a endpoint with the given options' do
+ expect(client.api.connection_options.dig(:headers, :host)).to eq 'try.gitea.io'
+ expect(client.api.api_endpoint).to eq 'https://167.99.148.217/api/v1/'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index a8472062f03..3bc0bd385a7 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
context 'when the user password is expired' do
- let(:actor) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true) }
+ let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be false
@@ -135,12 +135,12 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
context 'when the actor is an ldap user' do
- before do
- allow(actor).to receive(:ldap_user?).and_return(true)
- end
+ let(:actor) { create(:omniauth_user, provider: 'ldap') }
context 'when the user is blocked' do
- let(:actor) { create(:user, :blocked) }
+ before do
+ actor.block!
+ end
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be false
@@ -148,7 +148,9 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
context 'when the user password is expired' do
- let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
+ before do
+ actor.update!(password_expires_at: 1.minute.ago)
+ end
it 'returns true' do
expect(lfs_token.token_valid?(lfs_token.token)).to be true
diff --git a/spec/lib/gitlab/redis/rate_limiting_spec.rb b/spec/lib/gitlab/redis/rate_limiting_spec.rb
new file mode 100644
index 00000000000..f15aa71a52d
--- /dev/null
+++ b/spec/lib/gitlab/redis/rate_limiting_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Redis::RateLimiting do
+ let(:instance_specific_config_file) { "config/redis.rate_limiting.yml" }
+ let(:environment_config_file_name) { "GITLAB_REDIS_RATE_LIMITING_CONFIG_FILE" }
+ let(:cache_config_file) { nil }
+
+ before do
+ allow(Gitlab::Redis::Cache).to receive(:config_file_name).and_return(cache_config_file)
+ end
+
+ include_examples "redis_shared_examples"
+
+ describe '.config_file_name' do
+ subject { described_class.config_file_name }
+
+ let(:rails_root) { Dir.mktmpdir('redis_shared_examples') }
+
+ before do
+ # Undo top-level stub of config_file_name because we are testing that method now.
+ allow(described_class).to receive(:config_file_name).and_call_original
+
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ end
+
+ after do
+ FileUtils.rm_rf(rails_root)
+ end
+
+ context 'when there is only a resque.yml' do
+ before do
+ FileUtils.touch(File.join(rails_root, 'config/resque.yml'))
+ end
+
+ it { expect(subject).to eq("#{rails_root}/config/resque.yml") }
+
+ context 'and there is a global env override' do
+ before do
+ stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
+ end
+
+ it { expect(subject).to eq('global override') }
+
+ context 'and Cache has a different config file' do
+ let(:cache_config_file) { 'cache config file' }
+
+ it { expect(subject).to eq('cache config file') }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
index a02be83558c..0cbe44eacf4 100644
--- a/spec/lib/gitlab/string_regex_marker_spec.rb
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -23,9 +23,10 @@ RSpec.describe Gitlab::StringRegexMarker do
context 'with multiple occurrences' do
let(:raw) { %{a <b> <c> d} }
let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe }
+ let(:regexp) { /<[a-z]>/ }
subject do
- described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:, mode:|
+ described_class.new(raw, rich).mark(regexp) do |text, left:, right:, mode:|
%{<strong>#{text}</strong>}.html_safe
end
end
@@ -34,6 +35,15 @@ RSpec.describe Gitlab::StringRegexMarker do
expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
expect(subject).to be_html_safe
end
+
+ context 'with a Gitlab::UntrustedRegexp' do
+ let(:regexp) { Gitlab::UntrustedRegexp.new('<[a-z]>') }
+
+ it 'marks the matches' do
+ expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to be_html_safe
+ end
+ end
end
end
end
diff --git a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
new file mode 100644
index 00000000000..0d0f6a3df67
--- /dev/null
+++ b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('cleanup_orphan_project_access_tokens')
+
+RSpec.describe CleanupOrphanProjectAccessTokens, :migration do
+ def create_user(**extra_options)
+ defaults = { state: 'active', projects_limit: 0, email: "#{extra_options[:username]}@example.com" }
+
+ table(:users).create!(defaults.merge(extra_options))
+ end
+
+ def create_membership(**extra_options)
+ defaults = { access_level: 30, notification_level: 0, source_id: 1, source_type: 'Project' }
+
+ table(:members).create!(defaults.merge(extra_options))
+ end
+
+ let!(:regular_user) { create_user(username: 'regular') }
+ let!(:orphan_bot) { create_user(username: 'orphaned_bot', user_type: 6) }
+ let!(:used_bot) do
+ create_user(username: 'used_bot', user_type: 6).tap do |bot|
+ create_membership(user_id: bot.id)
+ end
+ end
+
+ it 'marks all bots without memberships as deactivated' do
+ expect do
+ migrate!
+ regular_user.reload
+ orphan_bot.reload
+ used_bot.reload
+ end.to change {
+ [regular_user.state, orphan_bot.state, used_bot.state]
+ }.from(%w[active active active]).to(%w[active deactivated active])
+ end
+
+ it 'schedules for deletion all bots without memberships' do
+ job_class = 'DeleteUserWorker'.safe_constantize
+
+ if job_class
+ expect(job_class).to receive(:bulk_perform_async).with([[orphan_bot.id, orphan_bot.id, skip_authorization: true]])
+
+ migrate!
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d25011059b2..1ef0d67958c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -5419,43 +5419,11 @@ RSpec.describe User do
end
describe '#password_expired_if_applicable?' do
- let(:user) { build(:user, password_expires_at: password_expires_at, password_automatically_set: set_automatically?) }
+ let(:user) { build(:user, password_expires_at: password_expires_at) }
subject { user.password_expired_if_applicable? }
- context 'when user is not ldap user' do
- context 'when user has password set automatically' do
- let(:set_automatically?) { true }
-
- context 'when password_expires_at is not set' do
- let(:password_expires_at) {}
-
- it 'returns false' do
- is_expected.to be_falsey
- end
- end
-
- context 'when password_expires_at is in the past' do
- let(:password_expires_at) { 1.minute.ago }
-
- it 'returns true' do
- is_expected.to be_truthy
- end
- end
-
- context 'when password_expires_at is in the future' do
- let(:password_expires_at) { 1.minute.from_now }
-
- it 'returns false' do
- is_expected.to be_falsey
- end
- end
- end
- end
-
- context 'when user has password not set automatically' do
- let(:set_automatically?) { false }
-
+ shared_examples 'password expired not applicable' do
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
@@ -5481,13 +5449,7 @@ RSpec.describe User do
end
end
- context 'when user is ldap user' do
- let(:user) { build(:user, password_expires_at: password_expires_at) }
-
- before do
- allow(user).to receive(:ldap_user?).and_return(true)
- end
-
+ context 'with a regular user' do
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
@@ -5499,8 +5461,8 @@ RSpec.describe User do
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
- it 'returns false' do
- is_expected.to be_falsey
+ it 'returns true' do
+ is_expected.to be_truthy
end
end
@@ -5513,32 +5475,26 @@ RSpec.describe User do
end
end
- context 'when user is a project bot' do
- let(:user) { build(:user, :project_bot, password_expires_at: password_expires_at) }
-
- context 'when password_expires_at is not set' do
- let(:password_expires_at) {}
-
- it 'returns false' do
- is_expected.to be_falsey
- end
+ context 'when user is a bot' do
+ before do
+ allow(user).to receive(:bot?).and_return(true)
end
- context 'when password_expires_at is in the past' do
- let(:password_expires_at) { 1.minute.ago }
+ it_behaves_like 'password expired not applicable'
+ end
- it 'returns false' do
- is_expected.to be_falsey
- end
- end
+ context 'when password_automatically_set is true' do
+ let(:user) { create(:omniauth_user, provider: 'ldap')}
- context 'when password_expires_at is in the future' do
- let(:password_expires_at) { 1.minute.from_now }
+ it_behaves_like 'password expired not applicable'
+ end
- it 'returns false' do
- is_expected.to be_falsey
- end
+ context 'when allow_password_authentication is false' do
+ before do
+ allow(user).to receive(:allow_password_authentication?).and_return(false)
end
+
+ it_behaves_like 'password expired not applicable'
end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 122612df355..ca9a5b1853c 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -249,15 +249,13 @@ RSpec.describe GlobalPolicy do
context 'user with expired password' do
before do
- current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_api) }
context 'when user is using ldap' do
- before do
- allow(current_user).to receive(:ldap_user?).and_return(true)
- end
+ let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it { is_expected.to be_allowed(:access_api) }
end
@@ -445,15 +443,13 @@ RSpec.describe GlobalPolicy do
context 'user with expired password' do
before do
- current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_git) }
context 'when user is using ldap' do
- before do
- allow(current_user).to receive(:ldap_user?).and_return(true)
- end
+ let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it { is_expected.to be_allowed(:access_git) }
end
@@ -537,15 +533,13 @@ RSpec.describe GlobalPolicy do
context 'user with expired password' do
before do
- current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true)
+ current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:use_slash_commands) }
context 'when user is using ldap' do
- before do
- allow(current_user).to receive(:ldap_user?).and_return(true)
- end
+ let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it { is_expected.to be_allowed(:use_slash_commands) }
end
diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb
index 2225f737f36..970416c7444 100644
--- a/spec/requests/api/import_bitbucket_server_spec.rb
+++ b/spec/requests/api/import_bitbucket_server_spec.rb
@@ -28,6 +28,20 @@ RSpec.describe API::ImportBitbucketServer do
Grape::Endpoint.before_each nil
end
+ it 'rejects requests when Bitbucket Server Importer is disabled' do
+ stub_application_setting(import_sources: nil)
+
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug
+ }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, repo_slug, user.namespace, user, anything)
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
new file mode 100644
index 00000000000..649647804c0
--- /dev/null
+++ b/spec/requests/api/integrations_spec.rb
@@ -0,0 +1,363 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe API::Integrations do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:project, reload: true) do
+ create(:project, creator_id: user.id, namespace: user.namespace)
+ end
+
+ %w[integrations services].each do |endpoint|
+ describe "GET /projects/:id/#{endpoint}" do
+ it 'returns authentication error when unauthenticated' do
+ get api("/projects/#{project.id}/#{endpoint}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it "returns error when authenticated but user is not a project owner" do
+ project.add_developer(user2)
+ get api("/projects/#{project.id}/#{endpoint}", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'with integrations' do
+ let!(:active_integration) { create(:emails_on_push_integration, project: project, active: true) }
+ let!(:integration) { create(:custom_issue_tracker_integration, project: project, active: false) }
+
+ it "returns a list of all active integrations" do
+ get api("/projects/#{project.id}/#{endpoint}", user)
+
+ aggregate_failures 'expect successful response with all active integrations' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['slug']).to eq('emails-on-push')
+ expect(response).to match_response_schema('public_api/v4/integrations')
+ end
+ end
+ end
+ end
+
+ Integration.available_integration_names.each do |integration|
+ describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
+ include_context integration
+
+ it "updates #{integration} settings" do
+ put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ current_integration = project.integrations.first
+ events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
+ query_strings = []
+ events.each do |event|
+ query_strings << "#{event}=#{!current_integration[event]}"
+ end
+ query_strings = query_strings.join('&')
+
+ put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}?#{query_strings}", user), params: integration_attrs
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['slug']).to eq(dashed_integration)
+ events.each do |event|
+ next if event == "foo"
+
+ expect(project.integrations.first[event]).not_to eq(current_integration[event]),
+ "expected #{!current_integration[event]} for event #{event} for #{endpoint} #{current_integration.title}, got #{current_integration[event]}"
+ end
+ end
+
+ it "returns if required fields missing" do
+ required_attributes = integration_attrs_list.select do |attr|
+ integration_klass.validators_on(attr).any? do |v|
+ v.instance_of?(ActiveRecord::Validations::PresenceValidator) &&
+ # exclude presence validators with conditional since those are not really required
+ ![:if, :unless].any? { |cond| v.options.include?(cond) }
+ end
+ end
+
+ if required_attributes.empty?
+ expected_code = :ok
+ else
+ integration_attrs.delete(required_attributes.sample)
+ expected_code = :bad_request
+ end
+
+ put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs
+
+ expect(response).to have_gitlab_http_status(expected_code)
+ end
+ end
+
+ describe "DELETE /projects/:id/#{endpoint}/#{integration.dasherize}" do
+ include_context integration
+
+ before do
+ initialize_integration(integration)
+ end
+
+ it "deletes #{integration}" do
+ delete api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ project.send(integration_method).reload
+ expect(project.send(integration_method).activated?).to be_falsey
+ end
+ end
+
+ describe "GET /projects/:id/#{endpoint}/#{integration.dasherize}" do
+ include_context integration
+
+ let!(:initialized_integration) { initialize_integration(integration, active: true) }
+
+ let_it_be(:project2) do
+ create(:project, creator_id: user.id, namespace: user.namespace)
+ end
+
+ def deactive_integration!
+ return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus)
+
+ # Integrations::Prometheus sets `#active` itself within a `before_save`:
+ initialized_integration.manual_configuration = false
+ initialized_integration.save!
+ end
+
+ it 'returns authentication error when unauthenticated' do
+ get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}")
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it "returns all properties of active integration #{integration}" do
+ get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user)
+
+ expect(initialized_integration).to be_active
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
+ end
+
+ it "returns all properties of inactive integration #{integration}" do
+ deactive_integration!
+
+ get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user)
+
+ expect(initialized_integration).not_to be_active
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
+ end
+
+ it "returns not found if integration does not exist" do
+ get api("/projects/#{project2.id}/#{endpoint}/#{dashed_integration}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Integration Not Found')
+ end
+
+ it "returns not found if integration exists but is in `Project#disabled_integrations`" do
+ expect_next_found_instance_of(Project) do |project|
+ expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration])
+ end
+
+ get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Integration Not Found')
+ end
+
+ it "returns error when authenticated but not a project owner" do
+ project.add_developer(user2)
+ get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user2)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{endpoint}/:slug/trigger" do
+ describe 'Mattermost integration' do
+ let(:integration_name) { 'mattermost_slash_commands' }
+
+ context 'when no integration is available' do
+ it 'returns a not found message' do
+ post api("/projects/#{project.id}/#{endpoint}/idonotexist/trigger")
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response["error"]).to eq("404 Not Found")
+ end
+ end
+
+ context 'when the integration exists' do
+ let(:params) { { token: 'token' } }
+
+ context 'when the integration is not active' do
+ before do
+ project.create_mattermost_slash_commands_integration(
+ active: false,
+ properties: params
+ )
+ end
+
+ it 'when the integration is inactive' do
+ post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the integration is active' do
+ before do
+ project.create_mattermost_slash_commands_integration(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'returns status 200' do
+ post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the project can not be found' do
+ it 'returns a generic 404' do
+ post api("/projects/404/#{endpoint}/#{integration_name}/trigger"), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response["message"]).to eq("404 Integration Not Found")
+ end
+ end
+ end
+ end
+
+ describe 'Slack Integration' do
+ let(:integration_name) { 'slack_slash_commands' }
+
+ before do
+ project.create_slack_slash_commands_integration(
+ active: true,
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'returns status 200' do
+ post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: { token: 'token', text: 'help' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['response_type']).to eq("ephemeral")
+ end
+ end
+ end
+
+ describe 'Mattermost integration' do
+ let(:integration_name) { 'mattermost' }
+ let(:params) do
+ { webhook: 'https://hook.example.com', username: 'username' }
+ end
+
+ before do
+ project.create_mattermost_integration(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'accepts a username for update' do
+ put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(username: 'new_username')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties']['username']).to eq('new_username')
+ end
+ end
+
+ describe 'Microsoft Teams integration' do
+ let(:integration_name) { 'microsoft-teams' }
+ let(:params) do
+ {
+ webhook: 'https://hook.example.com',
+ branches_to_be_notified: 'default',
+ notify_only_broken_pipelines: false
+ }
+ end
+
+ before do
+ project.create_microsoft_teams_integration(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'accepts branches_to_be_notified for update' do
+ put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user),
+ params: params.merge(branches_to_be_notified: 'all')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties']['branches_to_be_notified']).to eq('all')
+ end
+
+ it 'accepts notify_only_broken_pipelines for update' do
+ put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user),
+ params: params.merge(notify_only_broken_pipelines: true)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
+ end
+ end
+
+ describe 'Hangouts Chat integration' do
+ let(:integration_name) { 'hangouts-chat' }
+ let(:params) do
+ {
+ webhook: 'https://hook.example.com',
+ branches_to_be_notified: 'default'
+ }
+ end
+
+ before do
+ project.create_hangouts_chat_integration(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'accepts branches_to_be_notified for update', :aggregate_failures do
+ put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['properties']['branches_to_be_notified']).to eq('all')
+ end
+
+ it 'only requires the webhook param' do
+ put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: { webhook: 'https://hook.example.com' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ describe 'Pipelines Email Integration' do
+ let(:integration_name) { 'pipelines-email' }
+
+ context 'notify_only_broken_pipelines property was saved as a string' do
+ before do
+ project.create_pipelines_email_integration(
+ active: false,
+ properties: {
+ "notify_only_broken_pipelines": "true",
+ "branches_to_be_notified": "default"
+ }
+ )
+ end
+
+ it 'returns boolean values for notify_only_broken_pipelines' do
+ get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user)
+
+ expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 76a4548df8a..b23ba0021e0 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -259,22 +259,32 @@ RSpec.describe API::Invitations do
let(:route) { get invitations_url(source, stranger) }
end
- %i[maintainer developer access_requester stranger].each do |type|
+ context "when authenticated as a maintainer" do
+ it 'returns 200' do
+ get invitations_url(source, maintainer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ %i[developer access_requester stranger].each do |type|
context "when authenticated as a #{type}" do
- it 'returns 200' do
+ it 'returns 403' do
user = public_send(type)
get invitations_url(source, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(0)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
it 'avoids N+1 queries' do
+ invite_member_by_email(source, source_type, email, maintainer)
+
# Establish baseline
get invitations_url(source, maintainer)
@@ -282,7 +292,7 @@ RSpec.describe API::Invitations do
get invitations_url(source, maintainer)
end
- invite_member_by_email(source, source_type, email, maintainer)
+ invite_member_by_email(source, source_type, email2, maintainer)
expect do
get invitations_url(source, maintainer)
@@ -290,7 +300,7 @@ RSpec.describe API::Invitations do
end
it 'does not find confirmed members' do
- get invitations_url(source, developer)
+ get invitations_url(source, maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -300,10 +310,10 @@ RSpec.describe API::Invitations do
end
it 'finds all members with no query string specified' do
- invite_member_by_email(source, source_type, email, developer)
- invite_member_by_email(source, source_type, email2, developer)
+ invite_member_by_email(source, source_type, email, maintainer)
+ invite_member_by_email(source, source_type, email2, maintainer)
- get invitations_url(source, developer), params: { query: '' }
+ get invitations_url(source, maintainer), params: { query: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -314,17 +324,17 @@ RSpec.describe API::Invitations do
end
it 'finds the invitation by invite_email with query string' do
- invite_member_by_email(source, source_type, email, developer)
- invite_member_by_email(source, source_type, email2, developer)
+ invite_member_by_email(source, source_type, email, maintainer)
+ invite_member_by_email(source, source_type, email2, maintainer)
- get invitations_url(source, developer), params: { query: email }
+ get invitations_url(source, maintainer), params: { query: email }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['invite_email']).to eq(email)
- expect(json_response.first['created_by_name']).to eq(developer.name)
+ expect(json_response.first['created_by_name']).to eq(maintainer.name)
expect(json_response.first['user_name']).to eq(nil)
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 80bccdfee0c..be8a6c7bdcf 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1149,6 +1149,16 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ it 'disallows creating a project with an import_url when git import source is disabled' do
+ stub_application_setting(import_sources: nil)
+
+ project_params = { import_url: 'http://example.com', path: 'path-project-Foo', name: 'Foo Project' }
+ expect { post api('/projects', user), params: project_params }
+ .not_to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
it 'sets a project as public' do
project = attributes_for(:project, visibility: 'public')
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
deleted file mode 100644
index e550132e776..00000000000
--- a/spec/requests/api/services_spec.rb
+++ /dev/null
@@ -1,361 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-RSpec.describe API::Services do
- let_it_be(:user) { create(:user) }
- let_it_be(:user2) { create(:user) }
-
- let_it_be(:project, reload: true) do
- create(:project, creator_id: user.id, namespace: user.namespace)
- end
-
- describe "GET /projects/:id/services" do
- it 'returns authentication error when unauthenticated' do
- get api("/projects/#{project.id}/services")
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it "returns error when authenticated but user is not a project owner" do
- project.add_developer(user2)
- get api("/projects/#{project.id}/services", user2)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
- context 'with integrations' do
- let!(:active_integration) { create(:emails_on_push_integration, project: project, active: true) }
- let!(:integration) { create(:custom_issue_tracker_integration, project: project, active: false) }
-
- it "returns a list of all active integrations" do
- get api("/projects/#{project.id}/services", user)
-
- aggregate_failures 'expect successful response with all active integrations' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq(1)
- expect(json_response.first['slug']).to eq('emails-on-push')
- expect(response).to match_response_schema('public_api/v4/services')
- end
- end
- end
- end
-
- Integration.available_integration_names.each do |integration|
- describe "PUT /projects/:id/services/#{integration.dasherize}" do
- include_context integration
-
- it "updates #{integration} settings" do
- put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs
-
- expect(response).to have_gitlab_http_status(:ok)
-
- current_integration = project.integrations.first
- events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
- query_strings = []
- events.each do |event|
- query_strings << "#{event}=#{!current_integration[event]}"
- end
- query_strings = query_strings.join('&')
-
- put api("/projects/#{project.id}/services/#{dashed_integration}?#{query_strings}", user), params: integration_attrs
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['slug']).to eq(dashed_integration)
- events.each do |event|
- next if event == "foo"
-
- expect(project.integrations.first[event]).not_to eq(current_integration[event]),
- "expected #{!current_integration[event]} for event #{event} for service #{current_integration.title}, got #{current_integration[event]}"
- end
- end
-
- it "returns if required fields missing" do
- required_attributes = integration_attrs_list.select do |attr|
- integration_klass.validators_on(attr).any? do |v|
- v.instance_of?(ActiveRecord::Validations::PresenceValidator) &&
- # exclude presence validators with conditional since those are not really required
- ![:if, :unless].any? { |cond| v.options.include?(cond) }
- end
- end
-
- if required_attributes.empty?
- expected_code = :ok
- else
- integration_attrs.delete(required_attributes.sample)
- expected_code = :bad_request
- end
-
- put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs
-
- expect(response).to have_gitlab_http_status(expected_code)
- end
- end
-
- describe "DELETE /projects/:id/services/#{integration.dasherize}" do
- include_context integration
-
- before do
- initialize_integration(integration)
- end
-
- it "deletes #{integration}" do
- delete api("/projects/#{project.id}/services/#{dashed_integration}", user)
-
- expect(response).to have_gitlab_http_status(:no_content)
- project.send(integration_method).reload
- expect(project.send(integration_method).activated?).to be_falsey
- end
- end
-
- describe "GET /projects/:id/services/#{integration.dasherize}" do
- include_context integration
-
- let!(:initialized_integration) { initialize_integration(integration, active: true) }
-
- let_it_be(:project2) do
- create(:project, creator_id: user.id, namespace: user.namespace)
- end
-
- def deactive_integration!
- return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus)
-
- # Integrations::Prometheus sets `#active` itself within a `before_save`:
- initialized_integration.manual_configuration = false
- initialized_integration.save!
- end
-
- it 'returns authentication error when unauthenticated' do
- get api("/projects/#{project.id}/services/#{dashed_integration}")
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it "returns all properties of active service #{integration}" do
- get api("/projects/#{project.id}/services/#{dashed_integration}", user)
-
- expect(initialized_integration).to be_active
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
- end
-
- it "returns all properties of inactive integration #{integration}" do
- deactive_integration!
-
- get api("/projects/#{project.id}/services/#{dashed_integration}", user)
-
- expect(initialized_integration).not_to be_active
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
- end
-
- it "returns not found if integration does not exist" do
- get api("/projects/#{project2.id}/services/#{dashed_integration}", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Service Not Found')
- end
-
- it "returns not found if service exists but is in `Project#disabled_integrations`" do
- expect_next_found_instance_of(Project) do |project|
- expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration])
- end
-
- get api("/projects/#{project.id}/services/#{dashed_integration}", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Service Not Found')
- end
-
- it "returns error when authenticated but not a project owner" do
- project.add_developer(user2)
- get api("/projects/#{project.id}/services/#{dashed_integration}", user2)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- describe 'POST /projects/:id/services/:slug/trigger' do
- describe 'Mattermost integration' do
- let(:integration_name) { 'mattermost_slash_commands' }
-
- context 'when no integration is available' do
- it 'returns a not found message' do
- post api("/projects/#{project.id}/services/idonotexist/trigger")
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response["error"]).to eq("404 Not Found")
- end
- end
-
- context 'when the integration exists' do
- let(:params) { { token: 'token' } }
-
- context 'when the integration is not active' do
- before do
- project.create_mattermost_slash_commands_integration(
- active: false,
- properties: params
- )
- end
-
- it 'when the integration is inactive' do
- post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'when the integration is active' do
- before do
- project.create_mattermost_slash_commands_integration(
- active: true,
- properties: params
- )
- end
-
- it 'returns status 200' do
- post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when the project can not be found' do
- it 'returns a generic 404' do
- post api("/projects/404/services/#{integration_name}/trigger"), params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response["message"]).to eq("404 Service Not Found")
- end
- end
- end
- end
-
- describe 'Slack Integration' do
- let(:integration_name) { 'slack_slash_commands' }
-
- before do
- project.create_slack_slash_commands_integration(
- active: true,
- properties: { token: 'token' }
- )
- end
-
- it 'returns status 200' do
- post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: { token: 'token', text: 'help' }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['response_type']).to eq("ephemeral")
- end
- end
- end
-
- describe 'Mattermost integration' do
- let(:integration_name) { 'mattermost' }
- let(:params) do
- { webhook: 'https://hook.example.com', username: 'username' }
- end
-
- before do
- project.create_mattermost_integration(
- active: true,
- properties: params
- )
- end
-
- it 'accepts a username for update' do
- put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(username: 'new_username')
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['properties']['username']).to eq('new_username')
- end
- end
-
- describe 'Microsoft Teams integration' do
- let(:integration_name) { 'microsoft-teams' }
- let(:params) do
- {
- webhook: 'https://hook.example.com',
- branches_to_be_notified: 'default',
- notify_only_broken_pipelines: false
- }
- end
-
- before do
- project.create_microsoft_teams_integration(
- active: true,
- properties: params
- )
- end
-
- it 'accepts branches_to_be_notified for update' do
- put api("/projects/#{project.id}/services/#{integration_name}", user),
- params: params.merge(branches_to_be_notified: 'all')
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['properties']['branches_to_be_notified']).to eq('all')
- end
-
- it 'accepts notify_only_broken_pipelines for update' do
- put api("/projects/#{project.id}/services/#{integration_name}", user),
- params: params.merge(notify_only_broken_pipelines: true)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
- end
- end
-
- describe 'Hangouts Chat integration' do
- let(:integration_name) { 'hangouts-chat' }
- let(:params) do
- {
- webhook: 'https://hook.example.com',
- branches_to_be_notified: 'default'
- }
- end
-
- before do
- project.create_hangouts_chat_integration(
- active: true,
- properties: params
- )
- end
-
- it 'accepts branches_to_be_notified for update', :aggregate_failures do
- put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all')
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['properties']['branches_to_be_notified']).to eq('all')
- end
-
- it 'only requires the webhook param' do
- put api("/projects/#{project.id}/services/#{integration_name}", user), params: { webhook: 'https://hook.example.com' }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- describe 'Pipelines Email Integration' do
- let(:integration_name) { 'pipelines-email' }
-
- context 'notify_only_broken_pipelines property was saved as a string' do
- before do
- project.create_pipelines_email_integration(
- active: false,
- properties: {
- "notify_only_broken_pipelines": "true",
- "branches_to_be_notified": "default"
- }
- )
- end
-
- it 'returns boolean values for notify_only_broken_pipelines' do
- get api("/projects/#{project.id}/services/#{integration_name}", user)
-
- expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
- end
- end
- end
-end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 527e548ad19..ee1911b0a26 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -53,37 +53,6 @@ RSpec.describe API::Users do
end
end
- describe 'GET /users/:id' do
- context 'when unauthenticated' do
- it 'does not contain the note of the user' do
- get api("/users/#{user.id}")
-
- expect(json_response).not_to have_key('note')
- end
- end
-
- context 'when authenticated' do
- context 'as an admin' do
- it 'contains the note of the user' do
- get api("/users/#{user.id}", admin)
-
- expect(json_response).to have_key('note')
- expect(json_response['note']).to eq(user.note)
- expect(json_response).to have_key('sign_in_count')
- end
- end
-
- context 'as a regular user' do
- it 'does not contain the note of the user' do
- get api("/users/#{user.id}", user)
-
- expect(json_response).not_to have_key('note')
- expect(json_response).not_to have_key('sign_in_count')
- end
- end
- end
- end
-
describe "PUT /users/:id" do
context 'when user is an admin' do
it "updates note of the user" do
@@ -527,6 +496,8 @@ RSpec.describe API::Users do
end
describe "GET /users/:id" do
+ let_it_be(:user2, reload: true) { create(:user, username: 'another_user') }
+
it "returns a user by id" do
get api("/users/#{user.id}", user)
@@ -564,6 +535,64 @@ RSpec.describe API::Users do
expect(json_response.keys).not_to include 'trial'
end
+ it 'returns a 404 if the target user is present but inaccessible' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_user, user2).and_return(false)
+
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns the `created_at` field for public users' do
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include('created_at')
+ end
+
+ it 'does not return the `created_at` field for private users' do
+ get api("/users/#{private_user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include('created_at')
+ end
+
+ it 'returns the `followers` field for public users' do
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include('followers')
+ end
+
+ it 'does not return the `followers` field for private users' do
+ get api("/users/#{private_user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include('followers')
+ end
+
+ it 'returns the `following` field for public users' do
+ get api("/users/#{user2.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include('following')
+ end
+
+ it 'does not return the `following` field for private users' do
+ get api("/users/#{private_user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include('following')
+ end
+
+ it 'does not contain the note of the user' do
+ get api("/users/#{user.id}", user)
+
+ expect(json_response).not_to have_key('note')
+ expect(json_response).not_to have_key('sign_in_count')
+ end
+
context 'when job title is present' do
let(:job_title) { 'Fullstack Engineer' }
@@ -580,6 +609,14 @@ RSpec.describe API::Users do
end
context 'when authenticated as admin' do
+ it 'contains the note of the user' do
+ get api("/users/#{user.id}", admin)
+
+ expect(json_response).to have_key('note')
+ expect(json_response['note']).to eq(user.note)
+ expect(json_response).to have_key('sign_in_count')
+ end
+
it 'includes the `is_admin` field' do
get api("/users/#{user.id}", admin)
@@ -640,62 +677,10 @@ RSpec.describe API::Users do
end
context 'for an anonymous user' do
- it "returns a user by id" do
- get api("/users/#{user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response['username']).to eq(user.username)
- end
-
- it "returns a 404 if the target user is present but inaccessible" do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false)
-
- get api("/users/#{user.id}")
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns the `created_at` field for public users" do
- get api("/users/#{user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).to include 'created_at'
- end
-
- it "does not return the `created_at` field for private users" do
- get api("/users/#{private_user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).not_to include 'created_at'
- end
-
- it "returns the `followers` field for public users" do
- get api("/users/#{user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).to include 'followers'
- end
-
- it "does not return the `followers` field for private users" do
- get api("/users/#{private_user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).not_to include 'followers'
- end
-
- it "returns the `following` field for public users" do
+ it 'returns 403' do
get api("/users/#{user.id}")
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).to include 'following'
- end
-
- it "does not return the `following` field for private users" do
- get api("/users/#{private_user.id}")
-
- expect(response).to match_response_schema('public_api/v4/user/basic')
- expect(json_response.keys).not_to include 'following'
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -788,6 +773,14 @@ RSpec.describe API::Users do
describe 'GET /users/:id/followers' do
let(:follower) { create(:user) }
+ context 'for an anonymous user' do
+ it 'returns 403' do
+ get api("/users/#{user.id}")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
context 'user has followers' do
it 'lists followers' do
follower.follow(user)
@@ -823,6 +816,14 @@ RSpec.describe API::Users do
describe 'GET /users/:id/following' do
let(:followee) { create(:user) }
+ context 'for an anonymous user' do
+ it 'returns 403' do
+ get api("/users/#{user.id}")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
context 'user has followers' do
it 'lists following user' do
user.follow(followee)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index a16f5abf608..d2528600477 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'Git HTTP requests' do
shared_examples 'operations are not allowed with expired password' do
context "when password is expired" do
it "responds to downloads with status 401 Unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -69,7 +69,7 @@ RSpec.describe 'Git HTTP requests' do
end
it "responds to uploads with status 401 Unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -614,7 +614,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to downloads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -697,7 +697,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to uploads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
@@ -950,7 +950,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when users password is expired' do
it 'rejects pulls with 401 unauthorized' do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, user: 'gitlab-ci-token', password: build.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -1245,7 +1245,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to downloads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -1328,7 +1328,7 @@ RSpec.describe 'Git HTTP requests' do
context "when password is expired" do
it "responds to uploads with status 401 unauthorized" do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
@@ -1555,7 +1555,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when users password is expired' do
it 'rejects pulls with 401 unauthorized' do
- user.update!(password_expires_at: 2.days.ago, password_automatically_set: true)
+ user.update!(password_expires_at: 2.days.ago)
download(path, user: 'gitlab-ci-token', password: build.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 02eb4262690..656ae744ac1 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe 'Git LFS API and storage' do
it_behaves_like 'LFS http 200 blob response'
context 'when user password is expired' do
- let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true)}
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
it_behaves_like 'LFS http 401 response'
end
@@ -344,7 +344,7 @@ RSpec.describe 'Git LFS API and storage' do
end
context 'when user password is expired' do
- let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true)}
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
let(:role) { :reporter}
@@ -958,7 +958,7 @@ RSpec.describe 'Git LFS API and storage' do
it_behaves_like 'LFS http 200 workhorse response'
context 'when user password is expired' do
- let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true) }
+ let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) }
it_behaves_like 'LFS http 401 response'
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 904bfd3e7c3..6491c9ab65a 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -1144,17 +1144,28 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
end
context 'authenticated with lfs token' do
- it 'request is authenticated by token in basic auth' do
- lfs_token = Gitlab::LfsToken.new(user)
- encoded_login = ["#{user.username}:#{lfs_token.token}"].pack('m0')
+ let(:lfs_url) { '/namespace/repo.git/info/lfs/objects/batch' }
+ let(:lfs_token) { Gitlab::LfsToken.new(user) }
+ let(:encoded_login) { ["#{user.username}:#{lfs_token.token}"].pack('m0') }
+ let(:headers) { { 'AUTHORIZATION' => "Basic #{encoded_login}" } }
+ it 'request is authenticated by token in basic auth' do
expect_authenticated_request
- get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" }
+ get lfs_url, headers: headers
+ end
+
+ it 'request is not authenticated with API URL' do
+ expect_unauthenticated_request
+
+ get url, headers: headers
end
end
context 'authenticated with regular login' do
+ let(:encoded_login) { ["#{user.username}:#{user.password}"].pack('m0') }
+ let(:headers) { { 'AUTHORIZATION' => "Basic #{encoded_login}" } }
+
it 'request is authenticated after login' do
login_as(user)
@@ -1163,12 +1174,30 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
get url
end
- it 'request is authenticated by credentials in basic auth' do
- encoded_login = ["#{user.username}:#{user.password}"].pack('m0')
+ it 'request is not authenticated by credentials in basic auth' do
+ expect_unauthenticated_request
- expect_authenticated_request
+ get url, headers: headers
+ end
+
+ context 'with POST git-upload-pack' do
+ it 'request is authenticated by credentials in basic auth' do
+ expect(::Gitlab::Workhorse).to receive(:verify_api_request!)
+
+ expect_authenticated_request
- get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" }
+ post '/namespace/repo.git/git-upload-pack', headers: headers
+ end
+ end
+
+ context 'with GET info/refs' do
+ it 'request is authenticated by credentials in basic auth' do
+ expect(::Gitlab::Workhorse).to receive(:verify_api_request!)
+
+ expect_authenticated_request
+
+ get '/namespace/repo.git/info/refs?service=git-upload-pack', headers: headers
+ end
end
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index d09c9e692de..3fe585e87bb 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -452,6 +452,16 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
end
+ context 'when project has project bots' do
+ let!(:project_bot) { create(:user, :project_bot).tap { |user| project.add_maintainer(user) } }
+
+ it 'deletes bot user as well' do
+ expect do
+ destroy_project(project, user)
+ end.to change { User.find_by(id: project_bot.id) }.to(nil)
+ end
+ end
+
context 'error while destroying', :sidekiq_inline do
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) }
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index eeeb93fa811..946c8685741 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -38,4 +38,12 @@ RSpec.configure do |config|
redis_trace_chunks_cleanup!
end
+
+ config.around(:each, :clean_gitlab_redis_rate_limiting) do |example|
+ redis_rate_limiting_cleanup!
+
+ example.run
+
+ redis_rate_limiting_cleanup!
+ end
end
diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb
index 3511d906203..bf52da5d6f2 100644
--- a/spec/support/redis/redis_helpers.rb
+++ b/spec/support/redis/redis_helpers.rb
@@ -22,4 +22,9 @@ module RedisHelpers
def redis_trace_chunks_cleanup!
Gitlab::Redis::TraceChunks.with(&:flushdb)
end
+
+ # Usage: rate limiting state (for Rack::Attack)
+ def redis_rate_limiting_cleanup!
+ Gitlab::Redis::RateLimiting.with(&:flushdb)
+ end
end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 25eab5fd6e4..09bb3363205 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -327,6 +327,12 @@ RSpec.shared_examples "redis_shared_examples" do
expect(subject.send(:fetch_config)).to eq false
end
+
+ it 'has a value for the legacy default URL' do
+ allow(subject).to receive(:fetch_config) { false }
+
+ expect(subject.send(:raw_config_hash)).to include(url: a_string_matching(%r{\Aredis://localhost:638[012]\Z}))
+ end
end
def clear_raw_config
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index a20c1d78912..62b35923bcd 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -323,6 +323,16 @@ RSpec.shared_examples 'handle uploads authorize' do
end
end
+ context 'when id is not passed as a param' do
+ let(:params) { super().without(:id) }
+
+ it 'returns 404 status' do
+ post_authorize
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'when a user can upload a file' do
before do
sign_in(user)
diff --git a/storybook/config/main.js b/storybook/config/main.js
index c1369aebd91..59aa9deb3b5 100644
--- a/storybook/config/main.js
+++ b/storybook/config/main.js
@@ -1,4 +1,3 @@
-/* eslint-disable import/no-commonjs */
const IS_EE = require('../../config/helpers/is_ee_env');
module.exports = {
@@ -6,5 +5,5 @@ module.exports = {
'../../app/assets/javascripts/**/*.stories.js',
IS_EE && '../../ee/app/assets/javascripts/**/*.stories.js',
].filter(Boolean),
- addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'],
+ addons: ['@storybook/addon-essentials', '@storybook/addon-a11y', 'storybook-mirage'],
};
diff --git a/storybook/config/preview.js b/storybook/config/preview.js
index 4f496665de6..a55d0d52a0c 100644
--- a/storybook/config/preview.js
+++ b/storybook/config/preview.js
@@ -1,6 +1,7 @@
-// eslint-disable-next-line import/no-extraneous-dependencies
+import { withServer } from 'storybook-mirage'; // eslint-disable-line import/no-unresolved
import Vue from 'vue';
-import translateMixin from '../../app/assets/javascripts/vue_shared/translate';
+import { createMockServer } from 'test_helpers/mock_server';
+import translateMixin from '~/vue_shared/translate';
const stylesheetsRequireCtx = require.context(
'../../app/assets/stylesheets',
@@ -13,3 +14,5 @@ translateMixin(Vue);
stylesheetsRequireCtx('./application.scss');
stylesheetsRequireCtx('./application_utilities.scss');
+
+export const decorators = [withServer(createMockServer)];
diff --git a/storybook/config/webpack.config.js b/storybook/config/webpack.config.js
index 4382ba43f21..9d630dca970 100644
--- a/storybook/config/webpack.config.js
+++ b/storybook/config/webpack.config.js
@@ -5,6 +5,7 @@ const path = require('path');
const sass = require('node-sass'); // eslint-disable-line import/no-unresolved
const { buildIncludePaths, resolveGlobUrl } = require('node-sass-magic-importer/dist/toolbox'); // eslint-disable-line import/no-unresolved
const webpack = require('webpack');
+const IS_EE = require('../../config/helpers/is_ee_env');
const IS_JH = require('../../config/helpers/is_jh_env');
const gitlabWebpackConfig = require('../../config/webpack.config');
@@ -98,13 +99,31 @@ module.exports = function storybookWebpackConfig({ config }) {
},
],
},
+ {
+ test: /\.(graphql|gql)$/,
+ exclude: /node_modules/,
+ loader: 'graphql-tag/loader',
+ },
+ {
+ test: /\.(zip)$/,
+ loader: 'file-loader',
+ options: {
+ esModule: false,
+ },
+ },
];
// Silence webpack warnings about moment/pikaday not being able to resolve.
config.plugins.push(new webpack.IgnorePlugin(/moment/, /pikaday/));
+ const baseIntegrationTestHelpersPath = 'spec/frontend_integration/test_helpers';
+
// Add any missing aliases from the main GitLab webpack config
- Object.assign(config.resolve.alias, gitlabWebpackConfig.resolve.alias);
+ Object.assign(config.resolve.alias, gitlabWebpackConfig.resolve.alias, {
+ test_helpers: path.resolve(ROOT, baseIntegrationTestHelpersPath),
+ ee_else_ce_test_helpers: path.resolve(ROOT, IS_EE ? 'ee' : '', baseIntegrationTestHelpersPath),
+ test_fixtures: path.resolve(ROOT, 'tmp/tests/frontend', IS_EE ? 'fixtures-ee' : 'fixtures'),
+ });
// The main GitLab project aliases this `icons.svg` file to app/assets/javascripts/lib/utils/icons_path.js,
// which depends on the existence of a global `gon` variable.
// By deleting the alias, imports of this path will resolve as expected.
diff --git a/storybook/package.json b/storybook/package.json
index 4b922a4776c..48083ed1ce3 100644
--- a/storybook/package.json
+++ b/storybook/package.json
@@ -14,9 +14,11 @@
"@storybook/addon-controls": "^6.2.9",
"@storybook/addon-essentials": "^6.2.9",
"@storybook/vue": "6.2.9",
+ "graphql-tag": "^2.12.5",
"node-sass": "^4.14.1",
"node-sass-magic-importer": "^5.3.2",
"postcss-loader": "3.0.0",
- "sass-loader": "^7.1.0"
+ "sass-loader": "^7.1.0",
+ "storybook-mirage": "^0.0.4"
}
}
diff --git a/storybook/yarn.lock b/storybook/yarn.lock
index de00d3bcfc2..d63618ebc65 100644
--- a/storybook/yarn.lock
+++ b/storybook/yarn.lock
@@ -5388,6 +5388,13 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
+graphql-tag@^2.12.5:
+ version "2.12.5"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.5.tgz#5cff974a67b417747d05c8d9f5f3cb4495d0db8f"
+ integrity sha512-5xNhP4063d16Pz3HBtKprutsPrmHZi5IdUGOWRxA2B6VF7BIRGOHZ5WQvDmJXZuPcBg7rYwaFxvQYjqkSdR3TQ==
+ dependencies:
+ tslib "^2.1.0"
+
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
@@ -9546,6 +9553,11 @@ store2@^2.12.0:
resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf"
integrity sha512-7t+/wpKLanLzSnQPX8WAcuLCCeuSHoWdQuh9SB3xD0kNOM38DNf+0Oa+wmvxmYueRzkmh6IcdKFtvTa+ecgPDw==
+storybook-mirage@^0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/storybook-mirage/-/storybook-mirage-0.0.4.tgz#572e5d310ad8f0dd963e5c341aa2402f8ed07749"
+ integrity sha512-oGjsxyxmedXQtsVW1DDwKM1RocAD5zClFeOFtAhK46NcGXLJ31m2WQg5kL7YqrsriorrCZq4vvSy05DVCD7BKQ==
+
stream-browserify@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@@ -10057,6 +10069,11 @@ tslib@^2.0.1, tslib@^2.0.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
+tslib@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
+ integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
diff --git a/workhorse/internal/artifacts/entry.go b/workhorse/internal/artifacts/entry.go
index 91a0843f1ee..d5b3dfb672c 100644
--- a/workhorse/internal/artifacts/entry.go
+++ b/workhorse/internal/artifacts/entry.go
@@ -14,6 +14,7 @@ import (
"syscall"
"gitlab.com/gitlab-org/labkit/log"
+ "gitlab.com/gitlab-org/labkit/mask"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
@@ -35,7 +36,7 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string)
log.WithContextFields(r.Context(), log.Fields{
"entry": params.Entry,
- "archive": params.Archive,
+ "archive": mask.URL(params.Archive),
"path": r.URL.Path,
}).Print("SendEntry: sending")