summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-03 12:10:00 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-03 12:10:00 +0000
commit14a32c2d551a646525b1fabd93cb70a0e6924478 (patch)
tree782ba91ba786aee2cda379704e7f2ebcb5b46748
parent11c2f3b08c3bab4718a97360d1502f90793d028b (diff)
downloadgitlab-ce-14a32c2d551a646525b1fabd93cb70a0e6924478.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml5
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.checksum10
-rw-r--r--Gemfile.lock23
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js26
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue6
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js106
-rw-r--r--app/assets/javascripts/authentication/u2f/error.js26
-rw-r--r--app/assets/javascripts/authentication/u2f/index.js17
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js102
-rw-r--r--app/assets/javascripts/authentication/u2f/util.js40
-rw-r--r--app/assets/javascripts/authentication/webauthn/index.js20
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue2
-rw-r--r--app/assets/javascripts/pages/projects/boards/index.js2
-rw-r--r--app/assets/javascripts/sentry/constants.js2
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue14
-rw-r--r--app/assets/stylesheets/pages/profile.scss7
-rw-r--r--app/controllers/graphql_controller.rb14
-rw-r--r--app/models/user.rb6
-rw-r--r--app/views/admin/sessions/_two_factor_otp.html.haml2
-rw-r--r--app/views/admin/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml4
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml32
-rw-r--r--config/feature_flags/development/npm_allow_packages_in_multiple_projects.yml (renamed from config/feature_flags/ops/advanced_user_search.yml)12
-rw-r--r--config/feature_flags/ops/advanced_user_index.yml8
-rw-r--r--danger/stable_branch_patch/Dangerfile2
-rw-r--r--db/migrate/20230217144421_add_check_type_to_pre_scan_step.rb11
-rw-r--r--db/post_migrate/20230302090155_add_async_index_on_unlocked_non_trace_job_artifacts_expire_at.rb15
-rw-r--r--db/schema_migrations/202302171444211
-rw-r--r--db/schema_migrations/202303020901551
-rw-r--r--db/structure.sql1
-rw-r--r--doc/administration/pages/index.md14
-rw-r--r--doc/api/graphql/reference/index.md13
-rw-r--r--doc/architecture/blueprints/composable_codebase_using_rails_engines/index.md92
-rw-r--r--doc/development/caching.md6
-rw-r--r--doc/development/cascading_settings.md36
-rw-r--r--doc/development/dangerbot.md28
-rw-r--r--doc/development/ee_features.md18
-rw-r--r--doc/development/fe_guide/accessibility.md49
-rw-r--r--doc/development/fe_guide/graphql.md22
-rw-r--r--doc/development/fe_guide/performance.md24
-rw-r--r--doc/development/fe_guide/storybook.md28
-rw-r--r--doc/development/fe_guide/style/vue.md274
-rw-r--r--doc/development/integrations/jira_connect.md22
-rw-r--r--doc/development/rails_update.md52
-rw-r--r--doc/development/service_ping/index.md38
-rw-r--r--doc/development/sidekiq/compatibility_across_updates.md150
-rw-r--r--doc/development/snowplow/implementation.md142
-rw-r--r--doc/development/spam_protection_and_captcha/model_and_services.md6
-rw-r--r--doc/development/testing_guide/best_practices.md2
-rw-r--r--doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md20
-rw-r--r--doc/development/testing_guide/flaky_tests.md6
-rw-r--r--doc/integration/omniauth.md8
-rw-r--r--doc/integration/saml.md6
-rw-r--r--doc/user/application_security/dast/browser_based.md8
-rw-r--r--doc/user/application_security/dast_api/index.md2
-rw-r--r--doc/user/profile/account/two_factor_authentication.md62
-rw-r--r--doc/user/profile/index.md10
-rw-r--r--doc/user/report_abuse.md2
-rw-r--r--doc/user/ssh.md6
-rw-r--r--lefthook.yml6
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb9
-rw-r--r--lib/api/helpers/packages/npm.rb32
-rw-r--r--lib/gitlab/ci/config.rb3
-rw-r--r--lib/gitlab/ci/config/external/file/base.rb2
-rw-r--r--lib/gitlab/ci/config/yaml.rb19
-rw-r--r--lib/gitlab/config/loader/multi_doc_yaml.rb65
-rw-r--r--lib/gitlab/config/loader/yaml.rb4
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake1
-rw-r--r--locale/gitlab.pot25
-rw-r--r--scripts/database/schema_validator.rb34
-rwxr-xr-xscripts/validate_schema_changes7
-rw-r--r--spec/controllers/admin/sessions_controller_spec.rb10
-rw-r--r--spec/controllers/graphql_controller_spec.rb35
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb8
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb9
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb9
-rw-r--r--spec/features/u2f_spec.rb216
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js104
-rw-r--r--spec/frontend/authentication/u2f/mock_u2f_device.js23
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js84
-rw-r--r--spec/frontend/authentication/u2f/util_spec.js61
-rw-r--r--spec/frontend/fixtures/u2f.rb48
-rw-r--r--spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb196
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb26
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb34
-rw-r--r--spec/scripts/database/schema_validator_spec.rb67
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/helpers/fake_u2f_device.rb47
-rw-r--r--spec/support/helpers/features/two_factor_helpers.rb10
-rw-r--r--spec/support/helpers/login_helpers.rb5
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb2
-rw-r--r--spec/support_specs/helpers/packages/npm_spec.rb133
-rw-r--r--spec/tooling/danger/stable_branch_spec.rb53
-rw-r--r--spec/views/admin/sessions/two_factor.html.haml_spec.rb10
-rw-r--r--tooling/danger/stable_branch.rb12
-rw-r--r--vendor/assets/javascripts/u2f.js750
104 files changed, 1389 insertions, 2451 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index ec7c87a6203..6bf014a6324 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -141,6 +141,8 @@ rspec-all frontend_fixture as-if-foss:
- !reference [.frontend-fixtures-base, needs]
- "compile-test-assets as-if-foss"
+# Uploads EE fixtures in the EE project.
+# Uploads FOSS fixtures in the FOSS project.
upload-frontend-fixtures:
extends:
- .frontend-fixtures-base
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 6c4171da36e..0332f5689bc 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -124,6 +124,9 @@
.if-foss-schedule: &if-foss-schedule
if: '$CI_PROJECT_PATH == "gitlab-org/gitlab-foss" && $CI_PIPELINE_SOURCE == "schedule"'
+.if-foss-default-branch: &if-foss-default-branch
+ if: '$CI_PROJECT_PATH == "gitlab-org/gitlab-foss" && $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'
+
.if-dot-com-gitlab-org-schedule: &if-dot-com-gitlab-org-schedule
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_PIPELINE_SOURCE == "schedule"'
@@ -996,6 +999,8 @@
when: never
- <<: *if-dot-com-gitlab-org-default-branch
changes: *code-backstage-patterns
+ - <<: *if-foss-default-branch
+ changes: *code-backstage-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes:
- ".gitlab/ci/frontend.gitlab-ci.yml"
diff --git a/Gemfile b/Gemfile
index ebf3cf7f5a3..5264e3786ba 100644
--- a/Gemfile
+++ b/Gemfile
@@ -470,6 +470,7 @@ group :test do
gem 'test-prof', '~> 1.0.7'
gem 'rspec_junit_formatter'
gem 'guard-rspec'
+ gem 'axe-core-rspec'
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
gem 'derailed_benchmarks', require: false
diff --git a/Gemfile.checksum b/Gemfile.checksum
index ab0800610b5..bc3e941251c 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -41,6 +41,9 @@
{"name":"aws-sdk-kms","version":"1.62.0","platform":"ruby","checksum":"b9111c698d783f3f092dcc6a8b9b7e3f53f00e6e501bdc5a4409afdcaf411a1c"},
{"name":"aws-sdk-s3","version":"1.119.1","platform":"ruby","checksum":"f7f5939f204839e20222c70823ec0dc5f10775e3538e102783f68b42f5a7f6e6"},
{"name":"aws-sigv4","version":"1.5.1","platform":"ruby","checksum":"d68c87fff4ee843b4b92b23c7f31f957f254ec6eb064181f7119124aab8b8bb4"},
+{"name":"axe-core-api","version":"4.6.0","platform":"ruby","checksum":"1b0ddec3353f108dc10363baf2282f43a5ff7f13d4e25f99071294e78f8a6c62"},
+{"name":"axe-core-rspec","version":"4.6.0","platform":"ruby","checksum":"11c25bc9dd388c137ba4e5e63d64d20092bf22c884d8ffc829a22acfbacd747f"},
+{"name":"axiom-types","version":"0.1.1","platform":"ruby","checksum":"c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383"},
{"name":"azure-storage-blob","version":"2.0.3","platform":"ruby","checksum":"61b76118843c91776bd24bee22c74adafeb7c4bb3a858a325047dae3b59d0363"},
{"name":"azure-storage-common","version":"2.0.4","platform":"ruby","checksum":"608f4daab0e06b583b73dcffd3246ea39e78056de31630286b0cf97af7d6956b"},
{"name":"babosa","version":"1.0.4","platform":"ruby","checksum":"18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99"},
@@ -80,6 +83,7 @@
{"name":"claide","version":"1.1.0","platform":"ruby","checksum":"6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e"},
{"name":"claide-plugins","version":"0.9.2","platform":"ruby","checksum":"c7ea78bc067ab23bce8515497cdcdcb8f01c86dadfbe13c44644e382922c1c2e"},
{"name":"coderay","version":"1.1.3","platform":"ruby","checksum":"dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b"},
+{"name":"coercible","version":"1.0.0","platform":"ruby","checksum":"5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc"},
{"name":"colored2","version":"3.1.2","platform":"ruby","checksum":"b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a"},
{"name":"commonmarker","version":"0.23.6","platform":"ruby","checksum":"c8aeaaaff4ba497bf180f762db63a0069794fafb6eff221224c9c8199d337b38"},
{"name":"concurrent-ruby","version":"1.2.0","platform":"ruby","checksum":"a5e799f71e7490f24a534d58c91380267d0ae306af0cdc518d6848b93475dae2"},
@@ -103,6 +107,7 @@
{"name":"default_value_for","version":"3.4.0","platform":"ruby","checksum":"35d2dc51675a6bedfa875778628d44b823e0d7336da9432519477174ebb0f40f"},
{"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"},
{"name":"derailed_benchmarks","version":"2.1.2","platform":"ruby","checksum":"eaadc6206ceeb5538ff8f5e04a0023d54ebdd95d04f33e8960fb95a5f189a14f"},
+{"name":"descendants_tracker","version":"0.0.4","platform":"ruby","checksum":"e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897"},
{"name":"device_detector","version":"1.0.0","platform":"ruby","checksum":"b800fb3150b00c23e87b6768011808ac1771fffaae74c3238ebaf2b782947a7d"},
{"name":"devise","version":"4.8.1","platform":"ruby","checksum":"fdd48bbe79a89e7c1152236a70479842ede48bea4fa7f4f2d8da1f872559803e"},
{"name":"devise-two-factor","version":"4.0.2","platform":"ruby","checksum":"6548d2696ed090d27046f888f4fa7380f151e0f823902d46fd9b91e7d0cac511"},
@@ -123,6 +128,7 @@
{"name":"dry-inflector","version":"0.2.0","platform":"ruby","checksum":"c7cf29c3dc9d961c115aac873ac39a4ff6988fae7f7871c473a9694c1f6fb39e"},
{"name":"dry-logic","version":"1.1.0","platform":"ruby","checksum":"eca4b39084c9d22778144b7e4cf8db20e8bab7de6d89deb220d20a9fde60b69d"},
{"name":"dry-types","version":"1.4.0","platform":"ruby","checksum":"68003bb0db3077fecd0270f4ae486a82ee76bab6d666fdc4e094380a67c9a1df"},
+{"name":"dumb_delegator","version":"1.0.0","platform":"ruby","checksum":"ff5e411816d2d8ad8e260b269e712ae3839dddb0f9f8e18d3b1a3fe08f6d2e94"},
{"name":"e2mmap","version":"0.1.0","platform":"ruby","checksum":"45ee6bba2d97a7d91ee0885774261feee87e28c598355df31e93b56196ec0f59"},
{"name":"ecma-re-validator","version":"0.3.0","platform":"ruby","checksum":"66a95bd8c2b0641baf1fbf9bd355a0dcf13c82c6883f6f496a722420a8b6e0d7"},
{"name":"ed25519","version":"1.3.0","platform":"java","checksum":"8e5d2f8a5325c7a463d61d1a48406ce54074c610f3dccd889e6532c9527a3894"},
@@ -288,6 +294,7 @@
{"name":"i18n_data","version":"0.13.1","platform":"ruby","checksum":"e5aa99b09a69b463bb0443fc1f9540351a49f3d1541c5e91316bafa035c63f66"},
{"name":"icalendar","version":"2.8.0","platform":"ruby","checksum":"e404f970c7572bdebf6f09f9890970b68aab400ba9e609dc7d46098f28d0ee87"},
{"name":"ice_cube","version":"0.16.4","platform":"ruby","checksum":"da117e5de24bdc33931be629f9b55048641924442c7e9b72fedc05e5592531b7"},
+{"name":"ice_nine","version":"0.11.2","platform":"ruby","checksum":"5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db"},
{"name":"imagen","version":"0.1.8","platform":"ruby","checksum":"fde7b727d4fe79c6bb5ac46c1f7184bf87a6d54df54d712ad2be039d2f93a162"},
{"name":"invisible_captcha","version":"2.0.0","platform":"ruby","checksum":"a381edcb1d1b8744e9dc398ecad142c3e2ab077604645f85eeb02f9ea535c042"},
{"name":"ipaddr","version":"1.2.2","platform":"ruby","checksum":"27916ee6367d549850d3675bc020f1f1ddafbbe1cfc58635f17dfa56c42f9f79"},
@@ -601,6 +608,8 @@
{"name":"test_file_finder","version":"0.1.4","platform":"ruby","checksum":"bc36d8339eac4fb9dc36514a7c5f4d389ac2fb6d010716fc715c5c8fbb98eacd"},
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
{"name":"thor","version":"1.2.1","platform":"ruby","checksum":"b1752153dc9c6b8d3fcaa665e9e1a00a3e73f28da5e238b81c404502e539d446"},
+{"name":"thread_safe","version":"0.3.6","platform":"java","checksum":"bb28394cd0924c068981adee71f36a81c85c92e7d74d3f62372bd51489a0e0c2"},
+{"name":"thread_safe","version":"0.3.6","platform":"ruby","checksum":"9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a"},
{"name":"thrift","version":"0.16.0","platform":"ruby","checksum":"d023286ea89e30444c9f1c28dd76107f87d8aaf85fe1742da1d8cd3b5417dcce"},
{"name":"tilt","version":"2.0.11","platform":"ruby","checksum":"7b180fc472cbdeb186c85d31c0f2d1e61a2c0d77e1d9fd0ca28482a9d972d6a0"},
{"name":"timeliness","version":"0.3.10","platform":"ruby","checksum":"c357233ce19dc53148e8b29dfddde134689f18f52b32928e9dfe12ebcf4a773f"},
@@ -642,6 +651,7 @@
{"name":"version_gem","version":"1.1.0","platform":"ruby","checksum":"6b009518020db57f51ec7b410213fae2bf692baea9f1b51770db97fbc93d9a80"},
{"name":"version_sorter","version":"2.3.0","platform":"ruby","checksum":"2147f2a1a3804fbb8f60d268b7d7c1ec717e6dd727ffe2c165b4e05e82efe1da"},
{"name":"view_component","version":"2.74.1","platform":"ruby","checksum":"0bbd47a9c11455a45043dc01aa604db708654718a4d8755c911425482e8392c0"},
+{"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"},
{"name":"vmstat","version":"2.3.0","platform":"ruby","checksum":"ab5446a3e3bd0a9cdb9d9ac69a0bbd119c4f161d945a0846a519dd7018af656d"},
{"name":"warden","version":"1.2.9","platform":"ruby","checksum":"46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0"},
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 484f8e8fd17..1d5c01c2b44 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -209,6 +209,17 @@ GEM
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
+ axe-core-api (4.6.0)
+ dumb_delegator
+ virtus
+ axe-core-rspec (4.6.0)
+ axe-core-api
+ dumb_delegator
+ virtus
+ axiom-types (0.1.1)
+ descendants_tracker (~> 0.0.4)
+ ice_nine (~> 0.11.0)
+ thread_safe (~> 0.3, >= 0.3.1)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
nokogiri (~> 1, >= 1.10.8)
@@ -283,6 +294,8 @@ GEM
nap
open4 (~> 1.3)
coderay (1.1.3)
+ coercible (1.0.0)
+ descendants_tracker (~> 0.0.1)
colored2 (3.1.2)
commonmarker (0.23.6)
concurrent-ruby (1.2.0)
@@ -342,6 +355,8 @@ GEM
rake (> 10, < 14)
ruby-statistics (>= 2.1)
thor (>= 0.19, < 2)
+ descendants_tracker (0.0.4)
+ thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
devise (4.8.1)
bcrypt (~> 3.0)
@@ -391,6 +406,7 @@ GEM
dry-equalizer (~> 0.3)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
+ dumb_delegator (1.0.0)
e2mmap (0.1.0)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
@@ -771,6 +787,7 @@ GEM
icalendar (2.8.0)
ice_cube (~> 0.16)
ice_cube (0.16.4)
+ ice_nine (0.11.2)
imagen (0.1.8)
parser (>= 2.5, != 2.5.1.1)
invisible_captcha (2.0.0)
@@ -1450,6 +1467,7 @@ GEM
faraday (~> 1.0)
text (1.3.1)
thor (1.2.1)
+ thread_safe (0.3.6)
thrift (0.16.0)
tilt (2.0.11)
timeliness (0.3.10)
@@ -1530,6 +1548,10 @@ GEM
activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
+ virtus (2.0.0)
+ axiom-types (~> 0.1)
+ coercible (~> 1.0)
+ descendants_tracker (~> 0.0, >= 0.0.3)
vmstat (2.3.0)
warden (1.2.9)
rack (>= 2.0.9)
@@ -1596,6 +1618,7 @@ DEPENDENCIES
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.170.0)
aws-sdk-s3 (~> 1.119.1)
+ axe-core-rspec
babosa (~> 1.0.4)
base32 (~> 0.3.0)
batch-loader (~> 2.0.1)
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
index ebdcd1e074d..29dcab9ed4d 100644
--- a/app/assets/javascripts/authentication/mount_2fa.js
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -1,29 +1,9 @@
-import $ from 'jquery';
-import initU2F from './u2f';
-import U2FRegister from './u2f/register';
-import initWebauthnAuthentication from './webauthn';
-import WebAuthnRegister from './webauthn/register';
+import { initWebauthnAuthenticate, initWebauthnRegister } from './webauthn';
export const mount2faAuthentication = () => {
- if (gon.webauthn) {
- initWebauthnAuthentication();
- } else {
- initU2F();
- }
+ initWebauthnAuthenticate();
};
export const mount2faRegistration = () => {
- const el = $('#js-register-token-2fa');
-
- if (!el.length) {
- return;
- }
-
- if (gon.webauthn) {
- const webauthnRegister = new WebAuthnRegister(el, gon.webauthn);
- webauthnRegister.start();
- } else {
- const u2fRegister = new U2FRegister(el, gon.u2f);
- u2fRegister.start();
- }
+ initWebauthnRegister();
};
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
index 484c6524d0e..98ed2a31730 100644
--- 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
@@ -6,10 +6,8 @@ import { __ } from '~/locale';
export const i18n = {
currentPassword: __('Current password'),
confirmTitle: __('Are you sure?'),
- confirmWebAuthn: __(
- 'This will invalidate your registered applications and U2F / WebAuthn devices.',
- ),
- confirm: __('This will invalidate your registered applications and U2F devices.'),
+ confirmWebAuthn: __('This will invalidate your registered applications and WebAuthn devices.'),
+ confirm: __('This will invalidate your registered applications and WebAuthn devices.'),
disableTwoFactor: __('Disable two-factor authentication'),
disable: __('Disable'),
cancel: __('Cancel'),
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
deleted file mode 100644
index 22eca904f32..00000000000
--- a/app/assets/javascripts/authentication/u2f/authenticate.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import $ from 'jquery';
-import { template as lodashTemplate, omit } from 'lodash';
-import U2FError from './error';
-import importU2FLibrary from './util';
-
-// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
-//
-// State Flow #1: setup -> in_progress -> authenticated -> POST to server
-// State Flow #2: setup -> in_progress -> error -> setup
-export default class U2FAuthenticate {
- constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
- this.u2fUtils = null;
- this.container = container;
- this.renderAuthenticated = this.renderAuthenticated.bind(this);
- this.renderError = this.renderError.bind(this);
- this.renderInProgress = this.renderInProgress.bind(this);
- this.renderTemplate = this.renderTemplate.bind(this);
- this.authenticate = this.authenticate.bind(this);
- this.start = this.start.bind(this);
- this.appId = u2fParams.app_id;
- this.challenge = u2fParams.challenge;
- this.form = form;
- this.fallbackButton = fallbackButton;
- this.fallbackUI = fallbackUI;
- if (this.fallbackButton) {
- this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
- }
-
- // The U2F Javascript API v1.1 requires a single challenge, with
- // _no challenges per-request_. The U2F Javascript API v1.0 requires a
- // challenge per-request, which is done by copying the single challenge
- // into every request.
- //
- // In either case, we don't need the per-request challenges that the server
- // has generated, so we can remove them.
- //
- // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
- // This can be removed once we upgrade.
- // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
- this.signRequests = u2fParams.sign_requests.map((request) => omit(request, 'challenge'));
-
- this.templates = {
- inProgress: '#js-authenticate-token-2fa-in-progress',
- error: '#js-authenticate-token-2fa-error',
- authenticated: '#js-authenticate-token-2fa-authenticated',
- };
- }
-
- start() {
- return importU2FLibrary()
- .then((utils) => {
- this.u2fUtils = utils;
- this.renderInProgress();
- })
- .catch(() => this.switchToFallbackUI());
- }
-
- authenticate() {
- return this.u2fUtils.sign(
- this.appId,
- this.challenge,
- this.signRequests,
- (response) => {
- if (response.errorCode) {
- const error = new U2FError(response.errorCode, 'authenticate');
- return this.renderError(error);
- }
- return this.renderAuthenticated(JSON.stringify(response));
- },
- 10,
- );
- }
-
- renderTemplate(name, params) {
- const templateString = $(this.templates[name]).html();
- const template = lodashTemplate(templateString);
- return this.container.html(template(params));
- }
-
- renderInProgress() {
- this.renderTemplate('inProgress');
- return this.authenticate();
- }
-
- renderError(error) {
- this.renderTemplate('error', {
- error_message: error.message(),
- error_name: error.errorCode,
- });
- return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
- }
-
- renderAuthenticated(deviceResponse) {
- this.renderTemplate('authenticated');
- const container = this.container[0];
- container.querySelector('#js-device-response').value = deviceResponse;
- container.querySelector(this.form).submit();
- this.fallbackButton.classList.add('hidden');
- }
-
- switchToFallbackUI() {
- this.fallbackButton.classList.add('hidden');
- this.container[0].classList.add('hidden');
- this.fallbackUI.classList.remove('hidden');
- }
-}
diff --git a/app/assets/javascripts/authentication/u2f/error.js b/app/assets/javascripts/authentication/u2f/error.js
deleted file mode 100644
index ca0fc0700ad..00000000000
--- a/app/assets/javascripts/authentication/u2f/error.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { __ } from '~/locale';
-
-export default class U2FError {
- constructor(errorCode, u2fFlowType) {
- this.errorCode = errorCode;
- this.message = this.message.bind(this);
- this.httpsDisabled = window.location.protocol !== 'https:';
- this.u2fFlowType = u2fFlowType;
- }
-
- message() {
- if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
- return __(
- 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.',
- );
- } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
- if (this.u2fFlowType === 'authenticate') {
- return __('This device has not been registered with us.');
- }
- if (this.u2fFlowType === 'register') {
- return __('This device has already been registered with us.');
- }
- }
- return __('There was a problem communicating with your device.');
- }
-}
diff --git a/app/assets/javascripts/authentication/u2f/index.js b/app/assets/javascripts/authentication/u2f/index.js
deleted file mode 100644
index f129acca1c3..00000000000
--- a/app/assets/javascripts/authentication/u2f/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import $ from 'jquery';
-import U2FAuthenticate from './authenticate';
-
-export default () => {
- if (!gon.u2f) return;
-
- const u2fAuthenticate = new U2FAuthenticate(
- $('#js-authenticate-token-2fa'),
- '#js-login-token-2fa-form',
- gon.u2f,
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form'),
- );
- u2fAuthenticate.start();
- // needed in rspec (FakeU2fDevice)
- gl.u2fAuthenticate = u2fAuthenticate;
-};
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
deleted file mode 100644
index 6c98f0978bc..00000000000
--- a/app/assets/javascripts/authentication/u2f/register.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import $ from 'jquery';
-import { template as lodashTemplate } from 'lodash';
-import { __ } from '~/locale';
-import U2FError from './error';
-import importU2FLibrary from './util';
-
-// Register U2F (universal 2nd factor) devices for users to authenticate with.
-//
-// State Flow #1: setup -> in_progress -> registered -> POST to server
-// State Flow #2: setup -> in_progress -> error -> setup
-export default class U2FRegister {
- constructor(container, u2fParams) {
- this.u2fUtils = null;
- this.container = container;
- this.renderNotSupported = this.renderNotSupported.bind(this);
- this.renderRegistered = this.renderRegistered.bind(this);
- this.renderError = this.renderError.bind(this);
- this.renderInProgress = this.renderInProgress.bind(this);
- this.renderSetup = this.renderSetup.bind(this);
- this.renderTemplate = this.renderTemplate.bind(this);
- this.register = this.register.bind(this);
- this.start = this.start.bind(this);
- this.appId = u2fParams.app_id;
- this.registerRequests = u2fParams.register_requests;
- this.signRequests = u2fParams.sign_requests;
-
- this.templates = {
- message: '#js-register-2fa-message',
- setup: '#js-register-token-2fa-setup',
- error: '#js-register-token-2fa-error',
- registered: '#js-register-token-2fa-registered',
- };
- }
-
- start() {
- return importU2FLibrary()
- .then((utils) => {
- this.u2fUtils = utils;
- this.renderSetup();
- })
- .catch(() => this.renderNotSupported());
- }
-
- register() {
- return this.u2fUtils.register(
- this.appId,
- this.registerRequests,
- this.signRequests,
- (response) => {
- if (response.errorCode) {
- const error = new U2FError(response.errorCode, 'register');
- return this.renderError(error);
- }
- return this.renderRegistered(JSON.stringify(response));
- },
- 10,
- );
- }
-
- renderTemplate(name, params) {
- const templateString = $(this.templates[name]).html();
- const template = lodashTemplate(templateString);
- return this.container.html(template(params));
- }
-
- renderSetup() {
- this.renderTemplate('setup');
- return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
- }
-
- renderInProgress() {
- this.renderTemplate('message', {
- message: __(
- 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
- ),
- });
- return this.register();
- }
-
- renderError(error) {
- this.renderTemplate('error', {
- error_message: error.message(),
- error_name: error.errorCode,
- });
- return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
- }
-
- renderRegistered(deviceResponse) {
- this.renderTemplate('registered');
- // Prefer to do this instead of interpolating using Underscore templates
- // because of JSON escaping issues.
- return this.container.find('#js-device-response').val(deviceResponse);
- }
-
- renderNotSupported() {
- return this.renderTemplate('message', {
- message: __(
- "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).",
- ),
- });
- }
-}
diff --git a/app/assets/javascripts/authentication/u2f/util.js b/app/assets/javascripts/authentication/u2f/util.js
deleted file mode 100644
index b706481c02f..00000000000
--- a/app/assets/javascripts/authentication/u2f/util.js
+++ /dev/null
@@ -1,40 +0,0 @@
-function isOpera(userAgent) {
- return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0;
-}
-
-function getOperaVersion(userAgent) {
- const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/);
- return match ? parseInt(match[1], 10) : false;
-}
-
-function isChrome(userAgent) {
- return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent);
-}
-
-function getChromeVersion(userAgent) {
- const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
- return match ? parseInt(match[1], 10) : false;
-}
-
-export function canInjectU2fApi(userAgent) {
- const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41;
- const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40;
- const isMobile =
- userAgent.indexOf('droid') >= 0 ||
- userAgent.indexOf('CriOS') >= 0 ||
- /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
- return (isSupportedChrome || isSupportedOpera) && !isMobile;
-}
-
-export default function importU2FLibrary() {
- if (window.u2f) {
- return Promise.resolve(window.u2f);
- }
-
- const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
- if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) {
- return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f);
- }
-
- return Promise.reject();
-}
diff --git a/app/assets/javascripts/authentication/webauthn/index.js b/app/assets/javascripts/authentication/webauthn/index.js
index e9c20ce7795..1fbe89d1097 100644
--- a/app/assets/javascripts/authentication/webauthn/index.js
+++ b/app/assets/javascripts/authentication/webauthn/index.js
@@ -1,7 +1,12 @@
import $ from 'jquery';
-import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
+import WebAuthnAuthenticate from './authenticate';
+import WebAuthnRegister from './register';
+
+export const initWebauthnAuthenticate = () => {
+ if (!gon.webauthn) {
+ return;
+ }
-export default () => {
const webauthnAuthenticate = new WebAuthnAuthenticate(
$('#js-authenticate-token-2fa'),
'#js-login-token-2fa-form',
@@ -11,3 +16,14 @@ export default () => {
);
webauthnAuthenticate.start();
};
+
+export const initWebauthnRegister = () => {
+ const el = $('#js-register-token-2fa');
+
+ if (!el.length) {
+ return;
+ }
+
+ const webauthnRegister = new WebAuthnRegister(el, gon.webauthn);
+ webauthnRegister.start();
+};
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 36eb65c623b..4c25c0f97de 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -72,7 +72,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="packages-and-registries-group-settings">
<gl-alert v-if="alertMessage" variant="warning" class="gl-mt-4" @dismiss="dismissAlert">
{{ alertMessage }}
</gl-alert>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
index 11d8732426d..dd22d29d9a7 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
@@ -312,7 +312,7 @@ export default {
>
{{ __('Cancel') }}
</gl-button>
- <span class="gl-font-style-italic gl-text-gray-400">{{
+ <span class="gl-font-style-italic gl-text-gray-500">{{
$options.i18n.EXPIRATION_POLICY_FOOTER_NOTE
}}</span>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
index f06e3a41bd0..0bbb501011a 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
@@ -64,7 +64,7 @@ export default {
</gl-form-select>
</div>
<template v-if="description" #description>
- <span data-testid="description" class="gl-text-gray-400">
+ <span data-testid="description" class="gl-text-gray-500">
{{ description }}
</span>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
index 3fbbfd75ffb..749650e1060 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
@@ -101,7 +101,7 @@ export default {
trim
/>
<template #description>
- <span data-testid="description" class="gl-text-gray-400">
+ <span data-testid="description" class="gl-text-gray-500">
<gl-sprintf :message="description">
<template #link="{ content }">
<gl-link :href="tagsRegexHelpPagePath">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 2c1368262f2..4cc9cc190e8 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -42,7 +42,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="packages-and-registries-project-settings">
<gl-alert
v-if="showAlert"
variant="success"
diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js
index bde0007ec6a..23f5b083589 100644
--- a/app/assets/javascripts/pages/projects/boards/index.js
+++ b/app/assets/javascripts/pages/projects/boards/index.js
@@ -1,7 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
-import UsersSelect from '~/users_select';
-new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
diff --git a/app/assets/javascripts/sentry/constants.js b/app/assets/javascripts/sentry/constants.js
index 5531c4f56db..6bf1e2d3740 100644
--- a/app/assets/javascripts/sentry/constants.js
+++ b/app/assets/javascripts/sentry/constants.js
@@ -22,6 +22,8 @@ export const IGNORE_ERRORS = [
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
+ // Exclude errors from polling when navigating away from a page
+ 'TypeError: Failed to fetch',
];
export const DENY_URLS = [
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
index 8ec8482657d..8bb8b6101d4 100644
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ b/app/assets/javascripts/work_items/components/item_state.vue
@@ -63,7 +63,7 @@ export default {
:options="$options.states"
:disabled="disabled"
data-testid="work-item-state-select"
- class="gl-w-auto hide-select-decoration gl-pl-3"
+ class="gl-w-auto hide-select-decoration gl-pl-4 gl-my-1"
:class="{ 'gl-bg-transparent! gl-cursor-text!': disabled }"
@change="setState"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index fc4c05d96b2..95527dda1d4 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -298,7 +298,7 @@ export default {
<div class="form-row gl-mb-5 work-item-assignees gl-relative gl-flex-nowrap">
<span
:id="assigneesTitleId"
- class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
+ class="gl-font-weight-bold gl-mt-2 col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="assignees-title"
>{{ assigneeText }}</span
>
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 03c5b7096b2..3e546598dc2 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -223,7 +223,12 @@ export default {
@clear="clearStartDatePicker"
@close="handleStartDateInput"
/>
- <gl-button v-if="showStartDateButton" category="tertiary" @click="clickShowStartDate">
+ <gl-button
+ v-if="showStartDateButton"
+ category="tertiary"
+ class="gl-text-gray-500!"
+ @click="clickShowStartDate"
+ >
{{ $options.i18n.addStartDate }}
</gl-button>
</gl-form-group>
@@ -250,7 +255,12 @@ export default {
@clear="clearDueDatePicker"
@close="updateDates"
/>
- <gl-button v-if="showDueDateButton" category="tertiary" @click="clickShowDueDate">
+ <gl-button
+ v-if="showDueDateButton"
+ category="tertiary"
+ class="gl-text-gray-500!"
+ @click="clickShowDueDate"
+ >
{{ $options.i18n.addDueDate }}
</gl-button>
</gl-form-group>
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8e4dd39e498..41b022437bb 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -64,13 +64,6 @@
}
}
-table.u2f-registrations {
- th:not(:last-child),
- td:not(:last-child) {
- border-right: solid 1px transparent;
- }
-}
-
.codes {
padding-top: 14px;
}
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 2f01bdecd23..bf59a0a2400 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -102,6 +102,10 @@ class GraphqlController < ApplicationController
private
+ def permitted_params
+ params.permit(_json: [:query, :operationName, { variables: {} }])
+ end
+
def disallow_mutations_for_get
return unless request.get? || request.head?
return unless any_mutating_query?
@@ -111,7 +115,7 @@ class GraphqlController < ApplicationController
def limit_query_size
total_size = if multiplex?
- params[:_json].sum { _1[:query].size }
+ multiplex_param.sum { _1[:query].size }
else
query.size
end
@@ -178,8 +182,12 @@ class GraphqlController < ApplicationController
params.fetch(:query, '')
end
+ def multiplex_param
+ permitted_params[:_json]
+ end
+
def multiplex_queries
- params[:_json].map do |single_query_info|
+ multiplex_param.map do |single_query_info|
{
query: single_query_info[:query],
variables: build_variables(single_query_info[:variables]),
@@ -207,7 +215,7 @@ class GraphqlController < ApplicationController
end
def multiplex?
- params[:_json].present?
+ multiplex_param.present?
end
def authorize_access_api!
diff --git a/app/models/user.rb b/app/models/user.rb
index 421d2500f02..18c6a67700d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1063,7 +1063,7 @@ class User < ApplicationRecord
end
def two_factor_enabled?
- two_factor_otp_enabled? || two_factor_webauthn_u2f_enabled?
+ two_factor_otp_enabled? || two_factor_webauthn_enabled?
end
def two_factor_otp_enabled?
@@ -1082,10 +1082,6 @@ class User < ApplicationRecord
end
end
- def two_factor_webauthn_u2f_enabled?
- two_factor_u2f_enabled? || two_factor_webauthn_enabled?
- end
-
def two_factor_webauthn_enabled?
return false unless Feature.enabled?(:webauthn)
diff --git a/app/views/admin/sessions/_two_factor_otp.html.haml b/app/views/admin/sessions/_two_factor_otp.html.haml
index 037f0d5ecb1..f7b4035488d 100644
--- a/app/views/admin/sessions/_two_factor_otp.html.haml
+++ b/app/views/admin/sessions/_two_factor_otp.html.haml
@@ -1,4 +1,4 @@
-= form_tag(admin_session_path, { method: :post, class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if current_user.two_factor_webauthn_u2f_enabled?}" }) do
+= form_tag(admin_session_path, { method: :post, class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if current_user.two_factor_webauthn_enabled?}" }) do
.form-group
= label_tag :user_otp_attempt, _('Enter verification code')
= text_field_tag 'user[otp_attempt]', nil, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.')
diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml
index 3f915846dd8..86bc5af6c2a 100644
--- a/app/views/admin/sessions/two_factor.html.haml
+++ b/app/views/admin/sessions/two_factor.html.haml
@@ -11,5 +11,5 @@
.login-body
- if current_user.two_factor_otp_enabled?
= render 'admin/sessions/two_factor_otp'
- - if current_user.two_factor_webauthn_u2f_enabled?
+ - if current_user.two_factor_webauthn_enabled?
= render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 37b89f8233e..12e5a7263f7 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -3,7 +3,7 @@
.login-box.gl-p-5
.login-body
- if @user.two_factor_otp_enabled? || (Feature.enabled?(:webauthn_without_totp) && @user.two_factor_enabled?)
- = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_u2f_enabled?}" }) do |f|
+ = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_enabled?}" }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
@@ -12,5 +12,5 @@
%p.form-text.text-muted.hint= _("Enter the code from your two-factor authenticator app. If you've lost your device, you can enter one of your recovery codes.")
.prepend-top-20
= f.submit _("Verify code"), pajamas_button: true, data: { qa_selector: 'verify_code_button' }
- - if @user.two_factor_webauthn_u2f_enabled?
+ - if @user.two_factor_webauthn_enabled?
= render "authentication/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index c489b274f13..3b738b96ac4 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -64,39 +64,27 @@
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
- - if webauthn_enabled
- = _('Register a WebAuthn device')
- - else
- = _('Register a universal two-factor (U2F) device')
+ = _('Register a WebAuthn device')
%p
= _('Set up a hardware device to enable two-factor authentication (2FA).')
%p
- if webauthn_enabled && Feature.enabled?(:webauthn_without_totp)
= _("Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser.")
- - elsif webauthn_enabled
- = _("Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even from an unsupported browser.")
- else
- = _("Not all browsers support U2F devices. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even when you're using an unsupported browser.")
+ = _("Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in, even from an unsupported browser.")
.col-lg-8
- - registration = webauthn_enabled ? @webauthn_registration : @u2f_registration
- - if registration.errors.present?
- = form_errors(registration)
- - if webauthn_enabled
- = render "authentication/register", target_path: create_webauthn_profile_two_factor_auth_path
- - else
- = render "authentication/register", target_path: create_u2f_profile_two_factor_auth_path
+ - if @webauthn_registration.errors.present?
+ = form_errors(@webauthn_registration)
+ = render "authentication/register", target_path: create_webauthn_profile_two_factor_auth_path
%hr
%h5
- - if webauthn_enabled
- = _('WebAuthn Devices (%{length})') % { length: @registrations.length }
- - else
- = _('U2F Devices (%{length})') % { length: @registrations.length }
+ = _('WebAuthn Devices (%{length})') % { length: @registrations.length }
- if @registrations.present?
.table-responsive
- %table.table.table-bordered.u2f-registrations
+ %table.table
%colgroup
%col{ width: "50%" }
%col{ width: "30%" }
@@ -125,11 +113,7 @@
- else
.settings-message.text-center
- - if webauthn_enabled
- = _("You don't have any WebAuthn devices registered yet.")
- - else
- = _("You don't have any U2F devices registered yet.")
-
+ = _("You don't have any WebAuthn devices registered yet.")
%hr
diff --git a/config/feature_flags/ops/advanced_user_search.yml b/config/feature_flags/development/npm_allow_packages_in_multiple_projects.yml
index 0f52e9f22f0..7541a0dd24e 100644
--- a/config/feature_flags/ops/advanced_user_search.yml
+++ b/config/feature_flags/development/npm_allow_packages_in_multiple_projects.yml
@@ -1,8 +1,8 @@
---
-name: advanced_user_search
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102724
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382542
-milestone: '15.7'
-type: ops
-group: group::global search
+name: npm_allow_packages_in_multiple_projects
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111775
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391692
+milestone: '15.10'
+type: development
+group: group::package registry
default_enabled: false
diff --git a/config/feature_flags/ops/advanced_user_index.yml b/config/feature_flags/ops/advanced_user_index.yml
deleted file mode 100644
index 2aa33aa265a..00000000000
--- a/config/feature_flags/ops/advanced_user_index.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: advanced_user_index
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110946
-rollout_issue_url:
-milestone: '15.9'
-type: ops
-group: group::global_search
-default_enabled: true
diff --git a/danger/stable_branch_patch/Dangerfile b/danger/stable_branch_patch/Dangerfile
index 4fa8b05464a..2ac1b14c91f 100644
--- a/danger/stable_branch_patch/Dangerfile
+++ b/danger/stable_branch_patch/Dangerfile
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-if stable_branch.non_security_stable_branch?
+if stable_branch.encourage_package_and_qa_execution?
markdown(<<~MARKDOWN)
## QA `e2e:package-and-test`
diff --git a/db/migrate/20230217144421_add_check_type_to_pre_scan_step.rb b/db/migrate/20230217144421_add_check_type_to_pre_scan_step.rb
new file mode 100644
index 00000000000..e4b59c28d73
--- /dev/null
+++ b/db/migrate/20230217144421_add_check_type_to_pre_scan_step.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddCheckTypeToPreScanStep < Gitlab::Database::Migration[2.1]
+ def up
+ add_column :dast_pre_scan_verification_steps, :check_type, :integer, limit: 2, default: 0, null: false
+ end
+
+ def down
+ remove_column :dast_pre_scan_verification_steps, :check_type
+ end
+end
diff --git a/db/post_migrate/20230302090155_add_async_index_on_unlocked_non_trace_job_artifacts_expire_at.rb b/db/post_migrate/20230302090155_add_async_index_on_unlocked_non_trace_job_artifacts_expire_at.rb
new file mode 100644
index 00000000000..9f89b6916bd
--- /dev/null
+++ b/db/post_migrate/20230302090155_add_async_index_on_unlocked_non_trace_job_artifacts_expire_at.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddAsyncIndexOnUnlockedNonTraceJobArtifactsExpireAt < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_ci_job_artifacts_expire_at_unlocked_non_trace'
+
+ def up
+ prepare_async_index :ci_job_artifacts, :expire_at,
+ name: INDEX_NAME,
+ where: 'locked = 0 AND file_type != 3 AND expire_at IS NOT NULL'
+ end
+
+ def down
+ unprepare_async_index :ci_job_artifacts, :expire_at, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230217144421 b/db/schema_migrations/20230217144421
new file mode 100644
index 00000000000..1ab17fcfa99
--- /dev/null
+++ b/db/schema_migrations/20230217144421
@@ -0,0 +1 @@
+9a2ecdf9c37b13ebe5666ebadf2f27d4f52a0615337faaef221ff4fc6ae08cc4 \ No newline at end of file
diff --git a/db/schema_migrations/20230302090155 b/db/schema_migrations/20230302090155
new file mode 100644
index 00000000000..af86ce4635d
--- /dev/null
+++ b/db/schema_migrations/20230302090155
@@ -0,0 +1 @@
+52b9428336c506a0bf698ea03215dd2b279b9e7d2ca56936df094aaad7934f96 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index ac0b0b7c02c..d391a7ba109 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14741,6 +14741,7 @@ CREATE TABLE dast_pre_scan_verification_steps (
updated_at timestamp with time zone NOT NULL,
name text,
verification_errors text[] DEFAULT '{}'::text[] NOT NULL,
+ check_type smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_cd216b95e4 CHECK ((char_length(name) <= 255))
);
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index efd2f2039d1..ddae1014601 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -655,9 +655,9 @@ Follow the steps below to configure the proxy listener of GitLab Pages.
## Set global maximum size of each GitLab Pages site **(FREE SELF)**
-Prerequisites:
+Prerequisite:
-- Only GitLab administrators can edit this setting.
+- You must have administrator access to the instance.
To set the global maximum pages size for a project:
@@ -669,9 +669,9 @@ To set the global maximum pages size for a project:
## Set maximum size of each GitLab Pages site in a group **(PREMIUM SELF)**
-Prerequisites:
+Prerequisite:
-- You must have at least the Maintainer role for the group.
+- You must have administrator access to the instance.
To set the maximum size of each GitLab Pages site in a group, overriding the inherited setting:
@@ -683,9 +683,9 @@ To set the maximum size of each GitLab Pages site in a group, overriding the inh
## Set maximum size of GitLab Pages site in a project **(PREMIUM SELF)**
-Prerequisites:
+Prerequisite:
-- You must have at least the Maintainer role for the project.
+- You must have administrator access to the instance.
To set the maximum size of GitLab Pages site in a project, overriding the inherited setting:
@@ -701,7 +701,7 @@ To set the maximum size of GitLab Pages site in a project, overriding the inheri
Prerequisite:
-- You must be an administrator of a self-managed GitLab instance.
+- You must have administrator access to the instance.
To set the maximum number of GitLab Pages custom domains for a project:
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e0f34da8fbd..2866f811b71 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12328,8 +12328,9 @@ Represents a DAST Pre Scan Verification Step.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="dastprescanverificationstepchecktype"></a>`checkType` | [`DastPreScanVerificationCheckType`](#dastprescanverificationchecktype) | Type of the pre scan verification check. |
| <a id="dastprescanverificationsteperrors"></a>`errors` | [`[String!]`](#string) | Errors that occurred in the pre scan verification step. |
-| <a id="dastprescanverificationstepname"></a>`name` | [`String`](#string) | Name of the pre scan verification step. |
+| <a id="dastprescanverificationstepname"></a>`name` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.10. This was renamed. Use: [`DastPreScanVerificationStep.checkType`](#dastprescanverificationstepchecktype). |
| <a id="dastprescanverificationstepsuccess"></a>`success` | [`Boolean!`](#boolean) | Whether or not the pre scan verification step has errors. |
### `DastProfile`
@@ -22619,6 +22620,16 @@ Values for sorting tags.
| <a id="customerrelationsorganizationstateall"></a>`all` | All available organizations. |
| <a id="customerrelationsorganizationstateinactive"></a>`inactive` | Inactive organizations. |
+### `DastPreScanVerificationCheckType`
+
+Check type of the pre scan verification step.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="dastprescanverificationchecktypeauthentication"></a>`AUTHENTICATION` | Authentication check. |
+| <a id="dastprescanverificationchecktypeconnection"></a>`CONNECTION` | Connection check. |
+| <a id="dastprescanverificationchecktypecrawling"></a>`CRAWLING` | Crawling check. |
+
### `DastPreScanVerificationStatus`
Status of DAST pre scan verification.
diff --git a/doc/architecture/blueprints/composable_codebase_using_rails_engines/index.md b/doc/architecture/blueprints/composable_codebase_using_rails_engines/index.md
index 7fecbd1de71..12254fa7920 100644
--- a/doc/architecture/blueprints/composable_codebase_using_rails_engines/index.md
+++ b/doc/architecture/blueprints/composable_codebase_using_rails_engines/index.md
@@ -340,7 +340,7 @@ What was done?
spec.add_dependency 'graphql-docs'
spec.add_dependency 'grape'
end
- ```
+ ```
1. Move routes to the `engines/web_engine/config/routes.rb` file
@@ -380,59 +380,59 @@ What was done?
1. Configure GitLab when to load the engine.
- In GitLab `config/engines.rb`, we can configure when do we want to load our engines by relying on our `Gitlab::Runtime`
+ In GitLab `config/engines.rb`, we can configure when do we want to load our engines by relying on our `Gitlab::Runtime`
- ```ruby
- # config/engines.rb
- # Load only in case we are running web_server or rails console
- if Gitlab::Runtime.puma? || Gitlab::Runtime.console?
- require 'web_engine'
- end
- ```
+ ```ruby
+ # config/engines.rb
+ # Load only in case we are running web_server or rails console
+ if Gitlab::Runtime.puma? || Gitlab::Runtime.console?
+ require 'web_engine'
+ end
+ ```
1. Configure Engine
- Our Engine inherits from the `Rails::Engine` class. This way this gem notifies Rails that
- there's an engine at the specified path so it will correctly mount the engine inside
- the application, performing tasks such as adding the app directory of the engine to
- the load path for models, mailers, controllers, and views.
- A file at `lib/web_engine/engine.rb`, is identical in function to a standard Rails
- application's `config/application.rb` file. This way engines can access a configuration
- object which contains configuration shared by all railties and the application.
- Additionally, each engine can access `autoload_paths`, `eager_load_paths`, and `autoload_once_paths`
- settings which are scoped to that engine.
-
- ```ruby
- module WebEngine
- class Engine < ::Rails::Engine
- config.eager_load_paths.push(*%W[#{config.root}/lib
- #{config.root}/app/graphql/resolvers/concerns
- #{config.root}/app/graphql/mutations/concerns
- #{config.root}/app/graphql/types/concerns])
-
- if Gitlab.ee?
- ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
- ee_path = config.root
- .join('ee', Pathname.new(path).relative_path_from(config.root))
- memo << ee_path.to_s
- end
- # Eager load should load CE first
- config.eager_load_paths.push(*ee_paths)
- end
- end
- end
- ```
+ Our Engine inherits from the `Rails::Engine` class. This way this gem notifies Rails that
+ there's an engine at the specified path so it will correctly mount the engine inside
+ the application, performing tasks such as adding the app directory of the engine to
+ the load path for models, mailers, controllers, and views.
+ A file at `lib/web_engine/engine.rb`, is identical in function to a standard Rails
+ application's `config/application.rb` file. This way engines can access a configuration
+ object which contains configuration shared by all railties and the application.
+ Additionally, each engine can access `autoload_paths`, `eager_load_paths`, and `autoload_once_paths`
+ settings which are scoped to that engine.
+
+ ```ruby
+ module WebEngine
+ class Engine < ::Rails::Engine
+ config.eager_load_paths.push(*%W[#{config.root}/lib
+ #{config.root}/app/graphql/resolvers/concerns
+ #{config.root}/app/graphql/mutations/concerns
+ #{config.root}/app/graphql/types/concerns])
+
+ if Gitlab.ee?
+ ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
+ ee_path = config.root
+ .join('ee', Pathname.new(path).relative_path_from(config.root))
+ memo << ee_path.to_s
+ end
+ # Eager load should load CE first
+ config.eager_load_paths.push(*ee_paths)
+ end
+ end
+ end
+ ```
1. Testing
- We adapted CI to test `engines/web_engine/` as a self-sufficient component of stack.
+ We adapted CI to test `engines/web_engine/` as a self-sufficient component of stack.
- - We moved `spec` as-is files to the `engines/web_engine/spec` folder
- - We moved `ee/spec` as-is files to the `engines/web_engine/ee/spec` folder
- - We control specs from main application using environment variable `TEST_WEB_ENGINE`
- - We added new CI job that will run `engines/web_engine/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
- - We added new CI job that will run `engines/web_engine/ee/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
- - We are running all white box frontend tests with `TEST_WEB_ENGINE=true`
+ - We moved `spec` as-is files to the `engines/web_engine/spec` folder
+ - We moved `ee/spec` as-is files to the `engines/web_engine/ee/spec` folder
+ - We control specs from main application using environment variable `TEST_WEB_ENGINE`
+ - We added new CI job that will run `engines/web_engine/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
+ - We added new CI job that will run `engines/web_engine/ee/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
+ - We are running all white box frontend tests with `TEST_WEB_ENGINE=true`
#### Results
diff --git a/doc/development/caching.md b/doc/development/caching.md
index 9b3f9a4215e..dff94b68ca0 100644
--- a/doc/development/caching.md
+++ b/doc/development/caching.md
@@ -332,11 +332,11 @@ views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29/projects/16-20210316
```
1. The view name and template tree digest
- `views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29`
+ `views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29`
1. The model name, ID, and `updated_at` values
- `projects/16-20210316142425469452`
+ `projects/16-20210316142425469452`
1. The symbol we passed in, converted to a string
- `tag_list`
+ `tag_list`
### Look for
diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md
index 389623e68d8..42f4b5dd6f3 100644
--- a/doc/development/cascading_settings.md
+++ b/doc/development/cascading_settings.md
@@ -23,13 +23,13 @@ Settings are not cascading by default. To define a cascading setting, take the f
1. In the `NamespaceSetting` model, define the new attribute using the `cascading_attr`
helper method. You can use an array to define multiple attributes on a single line.
- ```ruby
- class NamespaceSetting
- include CascadingNamespaceSettingAttribute
+ ```ruby
+ class NamespaceSetting
+ include CascadingNamespaceSettingAttribute
- cascading_attr :delayed_project_removal
- end
- ```
+ cascading_attr :delayed_project_removal
+ end
+ ```
1. Create the database columns.
@@ -37,21 +37,21 @@ Settings are not cascading by default. To define a cascading setting, take the f
The helper creates four columns, two each in `namespace_settings` and
`application_settings`.
- ```ruby
- class AddDelayedProjectRemovalCascadingSetting < Gitlab::Database::Migration[2.1]
- include Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings
+ ```ruby
+ class AddDelayedProjectRemovalCascadingSetting < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings
- enable_lock_retries!
+ enable_lock_retries!
- def up
- add_cascading_namespace_setting :delayed_project_removal, :boolean, default: false, null: false
- end
+ def up
+ add_cascading_namespace_setting :delayed_project_removal, :boolean, default: false, null: false
+ end
- def down
- remove_cascading_namespace_setting :delayed_project_removal
- end
- end
- ```
+ def down
+ remove_cascading_namespace_setting :delayed_project_removal
+ end
+ end
+ ```
Existing settings being converted to a cascading setting will require individual
migrations to add columns and change existing columns. Use the specifications
diff --git a/doc/development/dangerbot.md b/doc/development/dangerbot.md
index b08eaed2afa..ef1e563b668 100644
--- a/doc/development/dangerbot.md
+++ b/doc/development/dangerbot.md
@@ -141,27 +141,27 @@ To enable the Dangerfile on another existing GitLab project, complete the follow
1. Add [`gitlab-dangerfiles`](https://rubygems.org/gems/gitlab-dangerfiles) to your `Gemfile`.
1. Create a `Dangerfile` with the following content:
- ```ruby
- require "gitlab-dangerfiles"
+ ```ruby
+ require "gitlab-dangerfiles"
- Gitlab::Dangerfiles.for_project(self, &:import_defaults)
- ```
+ Gitlab::Dangerfiles.for_project(self, &:import_defaults)
+ ```
1. Add the following to your CI/CD configuration:
- ```yaml
- include:
- - project: 'gitlab-org/quality/pipeline-common'
- file:
- - '/ci/danger-review.yml'
- rules:
- - if: $CI_SERVER_HOST == "gitlab.com"
- ```
+ ```yaml
+ include:
+ - project: 'gitlab-org/quality/pipeline-common'
+ file:
+ - '/ci/danger-review.yml'
+ rules:
+ - if: $CI_SERVER_HOST == "gitlab.com"
+ ```
1. If your project is in the `gitlab-org` group, you don't need to set up any token as the `DANGER_GITLAB_API_TOKEN`
variable is available at the group level. If not, follow these last steps:
- 1. Create a [Project access tokens](../user/project/settings/project_access_tokens.md).
- 1. Add the token as a CI/CD project variable named `DANGER_GITLAB_API_TOKEN`.
+ 1. Create a [Project access tokens](../user/project/settings/project_access_tokens.md).
+ 1. Add the token as a CI/CD project variable named `DANGER_GITLAB_API_TOKEN`.
You should add the ~"Danger bot" label to the merge request before sending it
for review.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 4eb5bedef1c..c57a598a8c2 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -74,9 +74,9 @@ To guard your licensed feature:
1. Optional. If your global feature is also available to namespaces with a paid plan, combine two
feature identifiers to allow both administrators and group users. For example:
- ```ruby
- License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # Both admins and group members can see this EE feature
- ```
+ ```ruby
+ License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # Both admins and group members can see this EE feature
+ ```
### Simulate a CE instance when unlicensed
@@ -100,15 +100,15 @@ To simulate a CE instance without deleting the license in a GDK:
1. Create an `env.runit` file in the root of your GDK with the line:
- ```shell
- export FOSS_ONLY=1
- ```
+ ```shell
+ export FOSS_ONLY=1
+ ```
1. Then restart the GDK:
- ```shell
- gdk restart rails && gdk restart webpack
- ```
+ ```shell
+ gdk restart rails && gdk restart webpack
+ ```
Remove the line in `env.runit` if you want to revert back to an EE
installation, and repeat step 2.
diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md
index f273c8133cb..b00131b12f3 100644
--- a/doc/development/fe_guide/accessibility.md
+++ b/doc/development/fe_guide/accessibility.md
@@ -516,6 +516,55 @@ GitLab-specific examples are assignee and label dropdowns.
Building such widgets require ARIA to make them understandable to screen readers.
Proper research and testing should be done to ensure compliance with [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/).
+## Automated accessibility testing with axe
+
+You can use [axe-core](https://github.com/dequelabs/axe-core) [gems](https://github.com/dequelabs/axe-core-gems)
+to run automated accessibility tests in feature tests.
+
+Axe provides the custom matcher `be_axe_clean`, which can be used like the following:
+
+```ruby
+# spec/features/settings_spec.rb
+it 'passes axe automated accessibility testing', :js do
+ visit_settings_page
+
+ wait_for_requests # ensures page is fully loaded
+
+ expect(page).to be_axe_clean
+end
+```
+
+If needed, you can scope testing to a specific area of the page by using `within`.
+
+Axe also provides specific [clauses](https://github.com/dequelabs/axe-core-gems/blob/develop/packages/axe-core-rspec/README.md#clauses),
+for example:
+
+```ruby
+expect(page).to be_axe_clean.within '[data-testid="element"]'
+
+# run only WCAG 2.0 Level AA rules
+expect(page).to be_axe_clean.according_to :wcag2aa
+
+# specifies which rule to skip
+expect(page).to be_axe_clean.skipping :'link-in-text-block'
+
+# clauses can be chained
+expect(page).to be_axe_clean.within('[data-testid="element"]')
+ .according_to(:wcag2aa)
+```
+
+Axe does not test hidden regions, such as inactive menus or modal windows. To test
+hidden regions for accessibility, write tests that activate or render the regions visible
+and run the matcher again.
+
+### Known accessibility violations
+
+This section documents violations where a recommendation differs with the [design system](https://design.gitlab.com/):
+
+- `link-in-text-block`: For now, use the `skipping` clause to skip `:'link-in-text-block'`
+ rule to fix the violation. After this is fixed as part of [issue 1444](https://gitlab.com/gitlab-org/gitlab-services/design.gitlab.com/-/issues/1444)
+ and underline is added to the `GlLink` component, this clause can be removed.
+
## Resources
### Viewing the browser accessibility tree
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 71a19b50001..7a79081b0d9 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -63,17 +63,17 @@ the GraphQL extension, follow these steps:
1. Add an `apollo.config.js` file to the root of your `gitlab` local directory.
1. Populate the file with the following content:
- ```javascript
- module.exports = {
- client: {
- includes: ['./app/assets/javascripts/**/*.graphql', './ee/app/assets/javascripts/**/*.graphql'],
- service: {
- name: 'GitLab',
- localSchemaFile: './tmp/tests/graphql/gitlab_schema.graphql',
- },
- },
- };
- ```
+ ```javascript
+ module.exports = {
+ client: {
+ includes: ['./app/assets/javascripts/**/*.graphql', './ee/app/assets/javascripts/**/*.graphql'],
+ service: {
+ name: 'GitLab',
+ localSchemaFile: './tmp/tests/graphql/gitlab_schema.graphql',
+ },
+ },
+ };
+ ```
1. Restart VS Code.
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index 3aa901093f0..20609718217 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -142,21 +142,21 @@ To use the Vue performance plugin:
1. Import the plugin:
- ```javascript
- import PerformancePlugin from '~/performance/vue_performance_plugin';
- ```
+ ```javascript
+ import PerformancePlugin from '~/performance/vue_performance_plugin';
+ ```
1. Use it before initializing your Vue application:
- ```javascript
- Vue.use(PerformancePlugin, {
- components: [
- 'IdeTreeList',
- 'FileTree',
- 'RepoEditor',
- ]
- });
- ```
+ ```javascript
+ Vue.use(PerformancePlugin, {
+ components: [
+ 'IdeTreeList',
+ 'FileTree',
+ 'RepoEditor',
+ ]
+ });
+ ```
The plugin accepts the list of components, performance of which should be measured. The components
should be specified by their `name` option.
diff --git a/doc/development/fe_guide/storybook.md b/doc/development/fe_guide/storybook.md
index e57c117bc39..8e0814ad96b 100644
--- a/doc/development/fe_guide/storybook.md
+++ b/doc/development/fe_guide/storybook.md
@@ -16,15 +16,15 @@ To build and launch Storybook locally, in the root directory of the `gitlab` pro
1. Install Storybook dependencies:
- ```shell
- yarn storybook:install
- ```
+ ```shell
+ yarn storybook:install
+ ```
1. Build the Storybook site:
- ```shell
- yarn storybook:start
- ```
+ ```shell
+ yarn storybook:start
+ ```
## Adding components to Storybook
@@ -35,14 +35,14 @@ To add a story:
1. Create a new `.stories.js` file in the same directory as the Vue component.
The filename should have the same prefix as the Vue component.
- ```txt
- vue_shared/
- ├─ components/
- │ ├─ sidebar
- │ | ├─ todo_toggle
- │ | | ├─ todo_button.vue
- │ │ | ├─ todo_button.stories.js
- ```
+ ```txt
+ vue_shared/
+ ├─ components/
+ │ ├─ sidebar
+ │ | ├─ todo_toggle
+ │ | | ├─ todo_button.vue
+ │ │ | ├─ todo_button.stories.js
+ ```
1. Write the story as per the [official Storybook instructions](https://storybook.js.org/docs/vue/writing-stories/introduction/)
diff --git a/doc/development/fe_guide/style/vue.md b/doc/development/fe_guide/style/vue.md
index a21d7c4577b..280019527da 100644
--- a/doc/development/fe_guide/style/vue.md
+++ b/doc/development/fe_guide/style/vue.md
@@ -59,63 +59,63 @@ Check the [rules](https://github.com/vuejs/eslint-plugin-vue#bulb-rules) for mor
1. Explicitly define data being passed into the Vue app
- ```javascript
- // bad
- return new Vue({
- el: '#element',
- components: {
- componentName
- },
- provide: {
- ...someDataset
- },
- props: {
- ...anotherDataset
- },
- render: createElement => createElement('component-name'),
- }));
-
- // good
- const { foobar, barfoo } = someDataset;
- const { foo, bar } = anotherDataset;
-
- return new Vue({
- el: '#element',
- components: {
- componentName
- },
- provide: {
- foobar,
- barfoo
- },
- props: {
- foo,
- bar
- },
- render: createElement => createElement('component-name'),
- }));
- ```
-
- We discourage the use of the spread operator in this specific case in
- order to keep our codebase explicit, discoverable, and searchable.
- This applies in any place where we would benefit from the above, such as
- when [initializing Vuex state](../vuex.md#why-not-just-spread-the-initial-state).
- The pattern above also enables us to easily parse non scalar values during
- instantiation.
-
- ```javascript
- return new Vue({
- el: '#element',
- components: {
- componentName
- },
- props: {
- foo,
- bar: parseBoolean(bar)
- },
- render: createElement => createElement('component-name'),
- }));
- ```
+ ```javascript
+ // bad
+ return new Vue({
+ el: '#element',
+ components: {
+ componentName
+ },
+ provide: {
+ ...someDataset
+ },
+ props: {
+ ...anotherDataset
+ },
+ render: createElement => createElement('component-name'),
+ }));
+
+ // good
+ const { foobar, barfoo } = someDataset;
+ const { foo, bar } = anotherDataset;
+
+ return new Vue({
+ el: '#element',
+ components: {
+ componentName
+ },
+ provide: {
+ foobar,
+ barfoo
+ },
+ props: {
+ foo,
+ bar
+ },
+ render: createElement => createElement('component-name'),
+ }));
+ ```
+
+ We discourage the use of the spread operator in this specific case in
+ order to keep our codebase explicit, discoverable, and searchable.
+ This applies in any place where we would benefit from the above, such as
+ when [initializing Vuex state](../vuex.md#why-not-just-spread-the-initial-state).
+ The pattern above also enables us to easily parse non scalar values during
+ instantiation.
+
+ ```javascript
+ return new Vue({
+ el: '#element',
+ components: {
+ componentName
+ },
+ props: {
+ foo,
+ bar: parseBoolean(bar)
+ },
+ render: createElement => createElement('component-name'),
+ }));
+ ```
## Naming
@@ -404,7 +404,7 @@ When using `v-for` you need to provide a *unique* `:key` attribute for each item
```
1. When using `v-for` with `template` and there is more than one child element, the `:key` values
-must be unique. It's advised to use `kebab-case` namespaces.
+ must be unique. It's advised to use `kebab-case` namespaces.
```html
<template v-for="(item, index) in items">
@@ -467,7 +467,7 @@ Creating a global, mutable wrapper provides a number of advantages, including th
```
- Use a `beforeEach` block to mount the component (see
-[the `createComponent` factory](#the-createcomponent-factory) for more information).
+ [the `createComponent` factory](#the-createcomponent-factory) for more information).
- Use an `afterEach` block to destroy the component, for example, `wrapper.destroy()`.
#### The `createComponent` factory
@@ -538,42 +538,42 @@ describe('MyComponent', () => {
#### `createComponent` best practices
1. Consider using a single (or a limited number of) object arguments over many arguments.
- Defining single parameters for common data like `props` is okay,
- but keep in mind our [JavaScript style guide](javascript.md#limit-number-of-parameters) and
- stay within the parameter number limit:
+ Defining single parameters for common data like `props` is okay,
+ but keep in mind our [JavaScript style guide](javascript.md#limit-number-of-parameters) and
+ stay within the parameter number limit:
- ```javascript
- // bad
- function createComponent(data, props, methods, isLoading, mountFn) { }
+ ```javascript
+ // bad
+ function createComponent(data, props, methods, isLoading, mountFn) { }
- // good
- function createComponent({ data, props, methods, stubs, isLoading } = {}) { }
+ // good
+ function createComponent({ data, props, methods, stubs, isLoading } = {}) { }
- // good
- function createComponent(props = {}, { data, methods, stubs, isLoading } = {}) { }
- ```
+ // good
+ function createComponent(props = {}, { data, methods, stubs, isLoading } = {}) { }
+ ```
1. If you require both `mount` _and_ `shallowMount` within the same set of tests, it
-can be useful define a `mountFn` parameter for the `createComponent` factory that accepts
-the mounting function (`mount` or `shallowMount`) to be used to mount the component:
+ can be useful define a `mountFn` parameter for the `createComponent` factory that accepts
+ the mounting function (`mount` or `shallowMount`) to be used to mount the component:
- ```javascript
- import { shallowMount } from '@vue/test-utils';
+ ```javascript
+ import { shallowMount } from '@vue/test-utils';
- function createComponent({ mountFn = shallowMount } = {}) { }
- ```
+ function createComponent({ mountFn = shallowMount } = {}) { }
+ ```
1. Use the `mountExtended` and `shallowMountExtended` helpers to expose `wrapper.findByTestId()`:
- ```javascript
- import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
- import { SomeComponent } from 'components/some_component.vue';
+ ```javascript
+ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+ import { SomeComponent } from 'components/some_component.vue';
- let wrapper;
+ let wrapper;
- const createWrapper = () => { wrapper = shallowMountExtended(SomeComponent); };
- const someButton = () => wrapper.findByTestId('someButtonTestId');
- ```
+ const createWrapper = () => { wrapper = shallowMountExtended(SomeComponent); };
+ const someButton = () => wrapper.findByTestId('someButtonTestId');
+ ```
### Setting component state
@@ -581,70 +581,70 @@ the mounting function (`mount` or `shallowMount`) to be used to mount the compon
component state wherever possible. Instead, set the component's
[`propsData`](https://v1.test-utils.vuejs.org/api/options.html#propsdata) when mounting the component:
- ```javascript
- // bad
- wrapper = shallowMount(MyComponent);
- wrapper.setProps({
- myProp: 'my cool prop'
- });
+ ```javascript
+ // bad
+ wrapper = shallowMount(MyComponent);
+ wrapper.setProps({
+ myProp: 'my cool prop'
+ });
- // good
- wrapper = shallowMount({ propsData: { myProp: 'my cool prop' } });
- ```
+ // good
+ wrapper = shallowMount({ propsData: { myProp: 'my cool prop' } });
+ ```
- The exception here is when you wish to test component reactivity in some way.
- For example, you may want to test the output of a component when after a particular watcher has
- executed. Using `setProps` to test such behavior is okay.
+ The exception here is when you wish to test component reactivity in some way.
+ For example, you may want to test the output of a component when after a particular watcher has
+ executed. Using `setProps` to test such behavior is okay.
### Accessing component state
1. When accessing props or attributes, prefer the `wrapper.props('myProp')` syntax over
`wrapper.props().myProp` or `wrapper.vm.myProp`:
- ```javascript
- // good
- expect(wrapper.props().myProp).toBe(true);
- expect(wrapper.attributes().myAttr).toBe(true);
+ ```javascript
+ // good
+ expect(wrapper.props().myProp).toBe(true);
+ expect(wrapper.attributes().myAttr).toBe(true);
- // better
- expect(wrapper.props('myProp').toBe(true);
- expect(wrapper.attributes('myAttr')).toBe(true);
- ```
+ // better
+ expect(wrapper.props('myProp').toBe(true);
+ expect(wrapper.attributes('myAttr')).toBe(true);
+ ```
1. When asserting multiple props, check the deep equality of the `props()` object with
[`toEqual`](https://jestjs.io/docs/expect#toequalvalue):
- ```javascript
- // good
- expect(wrapper.props('propA')).toBe('valueA');
- expect(wrapper.props('propB')).toBe('valueB');
- expect(wrapper.props('propC')).toBe('valueC');
-
- // better
- expect(wrapper.props()).toEqual({
- propA: 'valueA',
- propB: 'valueB',
- propC: 'valueC',
- });
- ```
+ ```javascript
+ // good
+ expect(wrapper.props('propA')).toBe('valueA');
+ expect(wrapper.props('propB')).toBe('valueB');
+ expect(wrapper.props('propC')).toBe('valueC');
+
+ // better
+ expect(wrapper.props()).toEqual({
+ propA: 'valueA',
+ propB: 'valueB',
+ propC: 'valueC',
+ });
+ ```
1. If you are only interested in some of the props, you can use
[`toMatchObject`](https://jestjs.io/docs/expect#tomatchobjectobject). Prefer `toMatchObject`
over [`expect.objectContaining`](https://jestjs.io/docs/expect#expectobjectcontainingobject):
- ```javascript
- // good
- expect(wrapper.props()).toEqual(expect.objectContaining({
- propA: 'valueA',
- propB: 'valueB',
- }));
+ ```javascript
+ // good
+ expect(wrapper.props()).toEqual(expect.objectContaining({
+ propA: 'valueA',
+ propB: 'valueB',
+ }));
- // better
- expect(wrapper.props()).toMatchObject({
- propA: 'valueA',
- propB: 'valueB',
- });
- ```
+ // better
+ expect(wrapper.props()).toMatchObject({
+ propA: 'valueA',
+ propB: 'valueB',
+ });
+ ```
## The JavaScript/Vue Accord
@@ -659,16 +659,16 @@ The goal of this accord is to make sure we are all on the same page.
1. We avoid adding new jQuery events when they are not required. Instead of adding new jQuery
events take a look at [different methods to do the same task](https://v2.vuejs.org/v2/api/#vm-emit).
1. You may query the `window` object one time, while bootstrapping your application for application
-specific data (for example, `scrollTo` is ok to access anytime). Do this access during the
-bootstrapping of your application.
+ specific data (for example, `scrollTo` is ok to access anytime). Do this access during the
+ bootstrapping of your application.
1. You may have a temporary but immediate need to create technical debt by writing code that does
-not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in
-the first place. An issue should be created for that tech debt to evaluate it further and discuss.
-In the coming months you should fix that tech debt, with its priority to be determined by maintainers.
+ not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in
+ the first place. An issue should be created for that tech debt to evaluate it further and discuss.
+ In the coming months you should fix that tech debt, with its priority to be determined by maintainers.
1. When creating tech debt you must write the tests for that code before hand and those tests may
-not be rewritten. For example, jQuery tests rewritten to Vue tests.
+ not be rewritten. For example, jQuery tests rewritten to Vue tests.
1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you
-must use the *store pattern* which can be found in the
-[Vue.js documentation](https://v2.vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
+ must use the *store pattern* which can be found in the
+ [Vue.js documentation](https://v2.vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
1. Once you have chosen a centralized state-management solution you must use it for your entire
-application. Don't mix and match your state-management solutions.
+ application. Don't mix and match your state-management solutions.
diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md
index eca4d9775c5..0b1fc8c6124 100644
--- a/doc/development/integrations/jira_connect.md
+++ b/doc/development/integrations/jira_connect.md
@@ -101,23 +101,23 @@ The following steps describe setting up an environment to test the GitLab OAuth
1. Start a Gitpod session and open the rails console.
- ```shell
- bundle exec rails console
- ```
+ ```shell
+ bundle exec rails console
+ ```
1. Enable the feature flag.
- ```shell
- Feature.enable(:jira_connect_oauth)
- ```
+ ```shell
+ Feature.enable(:jira_connect_oauth)
+ ```
1. On your GitLab instance, go to **Admin > Applications**.
1. Create a new application with the following settings:
- - Name: `Jira Connect`
- - Redirect URI: `YOUR_GITPOD_INSTANCE/-/jira_connect/oauth_callbacks`
- - Scopes: `api`
- - Trusted: **No**
- - Confidential: **No**
+ - Name: `Jira Connect`
+ - Redirect URI: `YOUR_GITPOD_INSTANCE/-/jira_connect/oauth_callbacks`
+ - Scopes: `api`
+ - Trusted: **No**
+ - Confidential: **No**
1. Copy the Application ID.
1. Go to **Admin > Settings > General**.
1. Scroll down and expand the GitLab for Jira App section.
diff --git a/doc/development/rails_update.md b/doc/development/rails_update.md
index a541de020fb..32295cc0e43 100644
--- a/doc/development/rails_update.md
+++ b/doc/development/rails_update.md
@@ -61,49 +61,49 @@ To efficiently and quickly find which Rails change caused the spec failure you c
1. Clone the `rails` project in a folder of your choice. For example, it might be the GDK root dir:
- ```shell
- cd <GDK_FOLDER>
- git clone https://github.com/rails/rails.git
- ```
+ ```shell
+ cd <GDK_FOLDER>
+ git clone https://github.com/rails/rails.git
+ ```
1. Replace the `gem 'rails'` line in GitLab `Gemfile` with:
- ```ruby
- gem 'rails', ENV['RAILS_VERSION'], path: ENV['RAILS_FOLDER']
- ```
+ ```ruby
+ gem 'rails', ENV['RAILS_VERSION'], path: ENV['RAILS_FOLDER']
+ ```
1. Set the `RAILS_FOLDER` environment variable with the folder you cloned Rails into:
- ```shell
- export RAILS_FOLDER="<GDK_FOLDER>/rails"
- ```
+ ```shell
+ export RAILS_FOLDER="<GDK_FOLDER>/rails"
+ ```
1. Change the directory to `RAILS_FOLDER` and set the range for the `git bisect` command:
- ```shell
- cd $RAILS_FOLDER
- git bisect start <NEW_VERSION_TAG> <OLD_VERSION_TAG>
- ```
+ ```shell
+ cd $RAILS_FOLDER
+ git bisect start <NEW_VERSION_TAG> <OLD_VERSION_TAG>
+ ```
- Where `<NEW_VERSION_TAG>` is the tag where the spec is red and `<OLD_VERSION_TAG>` is the one with the green spec.
- For example, `git bisect start v6.1.4.1 v6.1.3.2` if we're upgrading from version 6.1.3.2 to 6.1.4.1.
- Replace `<NEW_VERSION_TAG>` with the tag where the spec is red and `<OLD_VERSION_TAG>` with the one with the green spec. For example, `git bisect start v6.1.4.1 v6.1.3.2` if we're upgrading from version 6.1.3.2 to 6.1.4.1.
- In the output, you can see how many steps approximately it takes to find the commit.
+ Where `<NEW_VERSION_TAG>` is the tag where the spec is red and `<OLD_VERSION_TAG>` is the one with the green spec.
+ For example, `git bisect start v6.1.4.1 v6.1.3.2` if we're upgrading from version 6.1.3.2 to 6.1.4.1.
+ Replace `<NEW_VERSION_TAG>` with the tag where the spec is red and `<OLD_VERSION_TAG>` with the one with the green spec. For example, `git bisect start v6.1.4.1 v6.1.3.2` if we're upgrading from version 6.1.3.2 to 6.1.4.1.
+ In the output, you can see how many steps approximately it takes to find the commit.
1. Start the `git bisect` process and pass spec's filenames to `scripts/rails-update-bisect` as arguments. It can be faster to pick only one example instead of an entire spec file.
- ```shell
- git bisect run <GDK_FOLDER>/gitlab/scripts/rails-update-bisect spec/models/ability_spec.rb
- # OR
- git bisect run <GDK_FOLDER>/gitlab/scripts/rails-update-bisect spec/models/ability_spec.rb:7
- ```
+ ```shell
+ git bisect run <GDK_FOLDER>/gitlab/scripts/rails-update-bisect spec/models/ability_spec.rb
+ # OR
+ git bisect run <GDK_FOLDER>/gitlab/scripts/rails-update-bisect spec/models/ability_spec.rb:7
+ ```
1. When the process is completed, `git bisect` prints the commit hash, which you can use to find the corresponding MR in the [`rails/rails`](https://github.com/rails/rails) repository.
1. Execute `git bisect reset` to exit the `bisect` mode.
1. Revert the changes to `Gemfile`:
- ```shell
- git checkout -- Gemfile
- ```
+ ```shell
+ git checkout -- Gemfile
+ ```
### Follow-up reading material
diff --git a/doc/development/service_ping/index.md b/doc/development/service_ping/index.md
index 14c9cb33446..a5fc28b8d19 100644
--- a/doc/development/service_ping/index.md
+++ b/doc/development/service_ping/index.md
@@ -91,23 +91,23 @@ sequenceDiagram
the required URL is <https://version.gitlab.com/>.
1. In case of an error, it will be reported to the Version application along with following pieces of information:
- - `uuid` - GitLab instance unique identifier
- - `hostname` - GitLab instance hostname
- - `version` - GitLab instance current versions
- - `elapsed` - Amount of time which passed since Service Ping report process started and moment of error occurrence
- - `message` - Error message
-
- <pre>
- <code>
- {
- "uuid"=>"02333324-1cd7-4c3b-a45b-a4993f05fb1d",
- "hostname"=>"127.0.0.1",
- "version"=>"14.7.0-pre",
- "elapsed"=>0.006946,
- "message"=>'PG::UndefinedColumn: ERROR: column \"non_existent_attribute\" does not exist\nLINE 1: SELECT COUNT(non_existent_attribute) FROM \"issues\" /*applica...'
- }
- </code>
- </pre>
+ - `uuid` - GitLab instance unique identifier
+ - `hostname` - GitLab instance hostname
+ - `version` - GitLab instance current versions
+ - `elapsed` - Amount of time which passed since Service Ping report process started and moment of error occurrence
+ - `message` - Error message
+
+ <pre>
+ <code>
+ {
+ "uuid"=>"02333324-1cd7-4c3b-a45b-a4993f05fb1d",
+ "hostname"=>"127.0.0.1",
+ "version"=>"14.7.0-pre",
+ "elapsed"=>0.006946,
+ "message"=>'PG::UndefinedColumn: ERROR: column \"non_existent_attribute\" does not exist\nLINE 1: SELECT COUNT(non_existent_attribute) FROM \"issues\" /*applica...'
+ }
+ </code>
+ </pre>
1. Finally, the timing metadata information that is used for diagnostic purposes is submitted to the Versions application. It consists of a list of metric identifiers and the time it took to calculate the metrics:
@@ -135,7 +135,7 @@ sequenceDiagram
]
}
}
- ```
+```
### On a Geo secondary site
@@ -177,7 +177,7 @@ The following is example content of the Service Ping payload.
"recorded_at": "2020-04-17T07:43:54.162+00:00",
"edition": "EEU",
"license_md5": "00000000000000000000000000000000",
- "license_sha256: "0000000000000000000000000000000000000000000000000000000000000000",
+ "license_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"license_id": null,
"historical_max_users": 999,
"licensee": {
diff --git a/doc/development/sidekiq/compatibility_across_updates.md b/doc/development/sidekiq/compatibility_across_updates.md
index b417a099228..97663b0b41c 100644
--- a/doc/development/sidekiq/compatibility_across_updates.md
+++ b/doc/development/sidekiq/compatibility_across_updates.md
@@ -46,30 +46,30 @@ following example deprecates and then removes `arg2` from the `perform_async` me
1. Provide a default value (usually `nil`) and use a comment to mark the
argument as deprecated in the coming minor release. (Release M)
- ```ruby
- class ExampleWorker
- # Keep arg2 parameter for backwards compatibility.
- def perform(object_id, arg1, arg2 = nil)
- # ...
- end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ # Keep arg2 parameter for backwards compatibility.
+ def perform(object_id, arg1, arg2 = nil)
+ # ...
+ end
+ end
+ ```
1. One minor release later, stop using the argument in `perform_async`. (Release M+1)
- ```ruby
- ExampleWorker.perform_async(object_id, arg1)
- ```
+ ```ruby
+ ExampleWorker.perform_async(object_id, arg1)
+ ```
1. At the next major release, remove the value from the worker class. (Next major release)
- ```ruby
- class ExampleWorker
- def perform(object_id, arg1)
- # ...
- end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ def perform(object_id, arg1)
+ # ...
+ end
+ end
+ ```
### Add an argument
@@ -84,29 +84,29 @@ This approach requires multiple releases.
1. Add the argument to the worker with a default value (Release M).
- ```ruby
- class ExampleWorker
- def perform(object_id, new_arg = nil)
- # ...
- end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ def perform(object_id, new_arg = nil)
+ # ...
+ end
+ end
+ ```
1. Add the new argument to all the invocations of the worker (Release M+1).
- ```ruby
- ExampleWorker.perform_async(object_id, new_arg)
- ```
+ ```ruby
+ ExampleWorker.perform_async(object_id, new_arg)
+ ```
1. Remove the default value (Release M+2).
- ```ruby
- class ExampleWorker
- def perform(object_id, new_arg)
- # ...
- end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ def perform(object_id, new_arg)
+ # ...
+ end
+ end
+ ```
#### Parameter hash
@@ -115,13 +115,13 @@ uses a parameter hash.
1. Use a parameter hash in the worker to allow future flexibility.
- ```ruby
- class ExampleWorker
- def perform(object_id, params = {})
- # ...
- end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ def perform(object_id, params = {})
+ # ...
+ end
+ end
+ ```
## Removing worker classes
@@ -131,54 +131,54 @@ To remove a worker class, follow these steps over two minor releases:
1. Remove any code that enqueues the jobs.
- For example, if there is a UI component or an API endpoint that a user can interact with that results in the worker instance getting enqueued, make sure those surface areas are either removed or updated in a way that the worker instance is no longer enqueued.
+ For example, if there is a UI component or an API endpoint that a user can interact with that results in the worker instance getting enqueued, make sure those surface areas are either removed or updated in a way that the worker instance is no longer enqueued.
- This ensures that instances related to the worker class are no longer being enqueued.
+ This ensures that instances related to the worker class are no longer being enqueued.
1. Ensure both the frontend and backend code no longer relies on any of the work that used to be done by the worker.
1. In the relevant worker classes, replace the contents of the `perform` method with a no-op, while keeping any arguments in tact.
- For example, if you're working with the following `ExampleWorker`:
+ For example, if you're working with the following `ExampleWorker`:
- ```ruby
- class ExampleWorker
- def perform(object_id)
- SomeService.run!(object_id)
- end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ def perform(object_id)
+ SomeService.run!(object_id)
+ end
+ end
+ ```
- Implementing the no-op might look like this:
+ Implementing the no-op might look like this:
- ```ruby
- class ExampleWorker
- def perform(object_id); end
- end
- ```
+ ```ruby
+ class ExampleWorker
+ def perform(object_id); end
+ end
+ ```
- By implementing this no-op, you can avoid unnecessary cycles once any deprecated jobs that are still enqueued eventually get processed.
+ By implementing this no-op, you can avoid unnecessary cycles once any deprecated jobs that are still enqueued eventually get processed.
### In a subsequent, separate minor release
1. Delete the worker class file and follow the guidance in our [Sidekiq queues documentation](../sidekiq/index.md#sidekiq-queues) around running Rake tasks to regenerate/update related files.
1. Add a migration (not a post-deployment migration) that uses `sidekiq_remove_jobs`:
- ```ruby
- class RemoveMyDeprecatedWorkersJobInstances < Gitlab::Database::Migration[2.0]
- DEPRECATED_JOB_CLASSES = %w[
- MyDeprecatedWorkerOne
- MyDeprecatedWorkerTwo
- ]
-
- def up
- sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
- end
-
- def down
- # This migration removes any instances of deprecated workers and cannot be undone.
- end
- end
- ```
+ ```ruby
+ class RemoveMyDeprecatedWorkersJobInstances < Gitlab::Database::Migration[2.0]
+ DEPRECATED_JOB_CLASSES = %w[
+ MyDeprecatedWorkerOne
+ MyDeprecatedWorkerTwo
+ ]
+
+ def up
+ sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
+ end
+
+ def down
+ # This migration removes any instances of deprecated workers and cannot be undone.
+ end
+ end
+ ```
## Renaming queues
diff --git a/doc/development/snowplow/implementation.md b/doc/development/snowplow/implementation.md
index 40b8b7b3da8..a448e842bce 100644
--- a/doc/development/snowplow/implementation.md
+++ b/doc/development/snowplow/implementation.md
@@ -150,89 +150,89 @@ To implement Vue component tracking:
1. Import the `Tracking` library and call the `mixin` method:
- ```javascript
- import Tracking from '~/tracking';
+ ```javascript
+ import Tracking from '~/tracking';
- const trackingMixin = Tracking.mixin();
+ const trackingMixin = Tracking.mixin();
- // Optionally provide default properties
- // const trackingMixin = Tracking.mixin({ label: 'right_sidebar' });
- ```
+ // Optionally provide default properties
+ // const trackingMixin = Tracking.mixin({ label: 'right_sidebar' });
+ ```
1. Use the mixin in the component:
- ```javascript
- export default {
- mixins: [trackingMixin],
- // Or
- // mixins: [Tracking.mixin()],
- // mixins: [Tracking.mixin({ label: 'right_sidebar' })],
-
- data() {
- return {
- expanded: false,
- };
- },
- };
- ```
+ ```javascript
+ export default {
+ mixins: [trackingMixin],
+ // Or
+ // mixins: [Tracking.mixin()],
+ // mixins: [Tracking.mixin({ label: 'right_sidebar' })],
+
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ };
+ ```
1. You can specify tracking options in by creating a `tracking` data object
or computed property:
- ```javascript
- export default {
- name: 'RightSidebar',
-
- mixins: [Tracking.mixin()],
-
- data() {
- return {
- expanded: false,
- variant: '',
- tracking: {
- label: 'right_sidebar',
- // property: '',
- // value: '',
- // experiment: '',
- // extra: {},
- },
- };
- },
-
- // Or
- // computed: {
- // tracking() {
- // return {
- // property: this.variant,
- // extra: { expanded: this.expanded },
- // };
- // },
- // },
- };
- ```
+ ```javascript
+ export default {
+ name: 'RightSidebar',
+
+ mixins: [Tracking.mixin()],
+
+ data() {
+ return {
+ expanded: false,
+ variant: '',
+ tracking: {
+ label: 'right_sidebar',
+ // property: '',
+ // value: '',
+ // experiment: '',
+ // extra: {},
+ },
+ };
+ },
+
+ // Or
+ // computed: {
+ // tracking() {
+ // return {
+ // property: this.variant,
+ // extra: { expanded: this.expanded },
+ // };
+ // },
+ // },
+ };
+ ```
1. Call the `track` method. Tracking options can be passed as the second parameter:
- ```javascript
- this.track('click_button', {
- label: 'right_sidebar',
- });
- ```
-
- Or use the `track` method in the template:
-
- ```html
- <template>
- <div>
- <button data-testid="toggle" @click="toggle">Toggle</button>
-
- <div v-if="expanded">
- <p>Hello world!</p>
- <button @click="track('click_button')">Track another event</button>
- </div>
- </div>
- </template>
- ```
+ ```javascript
+ this.track('click_button', {
+ label: 'right_sidebar',
+ });
+ ```
+
+ Or use the `track` method in the template:
+
+ ```html
+ <template>
+ <div>
+ <button data-testid="toggle" @click="toggle">Toggle</button>
+
+ <div v-if="expanded">
+ <p>Hello world!</p>
+ <button @click="track('click_button')">Track another event</button>
+ </div>
+ </div>
+ </template>
+ ```
#### Testing example
diff --git a/doc/development/spam_protection_and_captcha/model_and_services.md b/doc/development/spam_protection_and_captcha/model_and_services.md
index 9c5d389a2f5..2c7ec8c3f50 100644
--- a/doc/development/spam_protection_and_captcha/model_and_services.md
+++ b/doc/development/spam_protection_and_captcha/model_and_services.md
@@ -35,9 +35,9 @@ To do this:
designate which fields to consider the "`title`" or "`description`". For example,
this line designates the `content` field as the `description`:
- ```ruby
- attr_spammable :content, spam_description: true
- ```
+ ```ruby
+ attr_spammable :content, spam_description: true
+ ```
1. Add a `#check_for_spam?` method implementation:
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 54d3f368bbd..a9d17472a9f 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -507,6 +507,8 @@ If needed, you can scope interactions within a specific area of the page by usin
As you will likely be scoping to an element such as a `div`, which typically does not have a label,
you may use a `data-testid` selector in this case.
+You can use the `be_axe_clean` matcher to run [axe automated accessibility testing](../fe_guide/accessibility.md#automated-accessibility-testing-with-axe) in feature tests.
+
##### Externalized contents
Test expectations against externalized contents should call the same
diff --git a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
index 4a947e59d5f..0d6d7a161c7 100644
--- a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
+++ b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
@@ -219,7 +219,7 @@ Geo requires an EE license. To visit the Geo sites in your browser, you need a r
# Using a full image address
GITLAB_QA_ACCESS_TOKEN=your-token-here gitlab-qa Test::Integration::Geo registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:examplesha123456789 --no-teardown
- ```
+ ```
You can use the `--no-tests` option to build the containers only, and then run the [`EE::Scenario::Test::Geo` scenario](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa/qa/ee/scenario/test/geo.rb) from your GDK to complete setup and run tests. However, there might be configuration issues if your GDK and the containers are based on different GitLab versions. With the `--no-teardown` option, GitLab-QA uses the same GitLab version for the GitLab containers and the GitLab QA container used to configure the Geo setup.
@@ -369,15 +369,15 @@ To run the LDAP tests on your local with TLS disabled, follow these steps:
1. Run the GitLab container:
- ```shell
- sudo docker run \
- --hostname localhost \
- --net test \
- --publish 443:443 --publish 80:80 --publish 22:22 \
- --name gitlab \
- --env GITLAB_OMNIBUS_CONFIG="gitlab_rails['ldap_enabled'] = true; gitlab_rails['ldap_servers'] = {\"main\"=>{\"label\"=>\"LDAP\", \"host\"=>\"ldap-server.test\", \"port\"=>389, \"uid\"=>\"uid\", \"bind_dn\"=>\"cn=admin,dc=example,dc=org\", \"password\"=>\"admin\", \"encryption\"=>\"plain\", \"verify_certificates\"=>false, \"base\"=>\"dc=example,dc=org\", \"user_filter\"=>\"\", \"group_base\"=>\"ou=Global Groups,dc=example,dc=org\", \"admin_group\"=>\"AdminGroup\", \"external_groups\"=>\"\", \"sync_ssh_keys\"=>false}}; gitlab_rails['ldap_sync_worker_cron'] = '* * * * *'; gitlab_rails['ldap_group_sync_worker_cron'] = '* * * * *'; " \
- gitlab/gitlab-ee:latest
- ```
+ ```shell
+ sudo docker run \
+ --hostname localhost \
+ --net test \
+ --publish 443:443 --publish 80:80 --publish 22:22 \
+ --name gitlab \
+ --env GITLAB_OMNIBUS_CONFIG="gitlab_rails['ldap_enabled'] = true; gitlab_rails['ldap_servers'] = {\"main\"=>{\"label\"=>\"LDAP\", \"host\"=>\"ldap-server.test\", \"port\"=>389, \"uid\"=>\"uid\", \"bind_dn\"=>\"cn=admin,dc=example,dc=org\", \"password\"=>\"admin\", \"encryption\"=>\"plain\", \"verify_certificates\"=>false, \"base\"=>\"dc=example,dc=org\", \"user_filter\"=>\"\", \"group_base\"=>\"ou=Global Groups,dc=example,dc=org\", \"admin_group\"=>\"AdminGroup\", \"external_groups\"=>\"\", \"sync_ssh_keys\"=>false}}; gitlab_rails['ldap_sync_worker_cron'] = '* * * * *'; gitlab_rails['ldap_group_sync_worker_cron'] = '* * * * *'; " \
+ gitlab/gitlab-ee:latest
+ ```
1. Run an LDAP test from [`gitlab/qa`](https://gitlab.com/gitlab-org/gitlab/-/tree/d5447ebb5f99d4c72780681ddf4dc25b0738acba/qa) directory:
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index f476815bf32..c626a4fa81c 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -251,9 +251,9 @@ which would give us the minimal test combination to reproduce the failure:
for the list under `Knapsack node specs:` in the CI job output log.
1. Save the list of specs as a file, and run:
- ```shell
- cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
- ```
+ ```shell
+ cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
+ ```
If there is an order-dependency issue, the script above will print the minimal
reproduction.
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 61019915c52..66d4ccb871f 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -379,8 +379,12 @@ but we'd like to at least help those with specific needs.
## Keep OmniAuth user profiles up to date
-You can enable profile syncing from selected OmniAuth providers. You can sync
-all or specific user information.
+You can enable profile syncing from selected OmniAuth providers.
+You can sync any combination of the following user attributes:
+
+- `name`
+- `email`
+- `location`
When authenticating using LDAP, the user's name and email are always synced.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 24b5e6152a5..888dd47a968 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -734,6 +734,10 @@ For a full list of supported assertions, see the [OmniAuth SAML gem](https://git
## Configure users based on SAML group membership
+NOTE:
+SAML Group Sync is only supported for the [SAML provider named `saml`](#configure-gitlab-to-use-multiple-saml-idps).
+As a result, SAML Group Sync only supports a single SAML provider. For more information, see [issue 366450](https://gitlab.com/gitlab-org/gitlab/-/issues/366450).
+
You can:
- Require users to be members of a certain group.
@@ -749,7 +753,7 @@ Support for these groups depends on:
- Whether you've installed [GitLab Enterprise Edition (EE)](https://about.gitlab.com/install/).
- The [name of the SAML provider](#configure-saml-support-in-gitlab). Group
memberships are only supported by a single SAML provider named
- `saml`. For more information, see [issue 386605](https://gitlab.com/gitlab-org/gitlab/-/issues/386605).
+ `saml`.
| Group | Tier | GitLab Enterprise Edition (EE) Only? |
|------------------------------|--------------------|--------------------------------------|
diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md
index bdc08988cc0..34c0bb59f67 100644
--- a/doc/user/application_security/dast/browser_based.md
+++ b/doc/user/application_security/dast/browser_based.md
@@ -42,10 +42,10 @@ To crawl a navigation path, DAST opens a browser window and instructs it to perf
When the browser has finished loading the result of the final action, DAST inspects the page for actions a user might take,
creates a new navigation for each found, and adds them to the navigation path to form new navigation paths. For example:
-- DAST processes navigation path `LoadURL[https://example.com]`.
-- DAST finds two user actions, `LeftClick[class=menu]` and `LeftClick[id=users]`.
-- DAST creates two new navigation paths, `LoadURL[https://example.com] -> LeftClick[class=menu]` and `LoadURL[https://example.com] -> LeftClick[id=users]`.
-- Crawling begins on the two new navigation paths.
+1. DAST processes navigation path `LoadURL[https://example.com]`.
+1. DAST finds two user actions, `LeftClick[class=menu]` and `LeftClick[id=users]`.
+1. DAST creates two new navigation paths, `LoadURL[https://example.com] -> LeftClick[class=menu]` and `LoadURL[https://example.com] -> LeftClick[id=users]`.
+1. Crawling begins on the two new navigation paths.
It's common for an HTML element to exist in multiple places in an application, such as a menu visible on every page.
Duplicate elements can cause crawlers to crawl the same pages again or become stuck in a loop.
diff --git a/doc/user/application_security/dast_api/index.md b/doc/user/application_security/dast_api/index.md
index 96c0b76dbec..d2353b2eb63 100644
--- a/doc/user/application_security/dast_api/index.md
+++ b/doc/user/application_security/dast_api/index.md
@@ -97,8 +97,6 @@ The environment variables `DAST_API_OPENAPI_ALL_MEDIA_TYPES` and `DAST_API_OPENA
#### Configure DAST API with an OpenAPI Specification
-To configure DAST API scanning with an OpenAPI specification:
-
To configure DAST API scanning with an OpenAPI Specification:
1. [Include](../../../ci/yaml/index.md#includetemplate)
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index b76b99b5242..ee6e3df772c 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -14,7 +14,7 @@ GitLab supports as a second factor of authentication:
- Time-based one-time passwords ([TOTP](https://datatracker.ietf.org/doc/html/rfc6238)). When enabled, GitLab prompts
you for a code when you sign in. Codes are generated by your one-time password authenticator (for example, a password
manager on one of your devices).
-- U2F or WebAuthn devices. You're prompted to activate your U2F or WebAuthn device (usually by pressing a button on it) when
+- WebAuthn devices. You're prompted to activate your WebAuthn device (usually by pressing a button on it) when
you supply your username and password to sign in. This performs secure authentication on your behalf.
If you set up a device, also set up a TOTP so you can still access your account if you lose the device.
@@ -42,10 +42,10 @@ Git Credential Manager is developed primarily by GitHub, Inc. It is an open-sour
> - Account email confirmation requirement [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35102) in GitLab 14.3. [Deployed behind the `ensure_verified_primary_email_for_2fa` flag](../../../administration/feature_flags.md), enabled by default.
> - Account email confirmation requirement generally available and [feature flag `ensure_verified_primary_email_for_2fa` removed](https://gitlab.com/gitlab-org/gitlab/-/issues/340151) in GitLab 14.4.
-You can enable 2FA:
+You can enable 2FA using a:
-- Using a one-time password authenticator. After you enable 2FA, back up your [recovery codes](#recovery-codes).
-- Using a U2F or WebAuthn device.
+- One-time password authenticator. After you enable 2FA, back up your [recovery codes](#recovery-codes).
+- WebAuthn device.
In GitLab 14.3 and later, your account email must be confirmed to enable 2FA.
@@ -198,33 +198,6 @@ Configure FortiToken Cloud in GitLab. On your GitLab server:
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) (Omnibus GitLab) or
[restart](../../../administration/restart_gitlab.md#installations-from-source) (GitLab installed from source).
-### Set up a U2F device
-
-GitLab officially supports [YubiKey](https://www.yubico.com/products/) U2F devices, but users have successfully used
-[SoloKeys](https://solokeys.com/) and [Google Titan Security Key](https://cloud.google.com/titan-security-key).
-
-U2F is [supported by](https://caniuse.com/#search=U2F) the following desktop browsers:
-
-- Chrome
-- Edge
-- Opera
-- Firefox 67+. For Firefox 47-66:
-
- 1. Enable the FIDO U2F API in [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
- 1. Search for `security.webauth.u2f` and select it to toggle to `true`.
-
-To set up 2FA with a U2F device:
-
-1. Access your [**User settings**](../index.md#access-your-user-settings).
-1. Select **Account**.
-1. Select **Enable Two-Factor Authentication**.
-1. Connect your U2F device.
-1. Select **Set up New U2F Device**.
-1. A light begins blinking on your device. Activate it by pressing its button.
-
-A message displays indicating that your device was successfully set up. Select **Register U2F Device** to complete the
-process. Recovery codes are not generated for U2F devices.
-
### Set up a WebAuthn device
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22506) in GitLab 13.4 [with a flag](../../../administration/feature_flags.md) named `webauthn`. Disabled by default.
@@ -276,7 +249,7 @@ We recommend copying and printing them, or downloading them using the **Download
place. If you choose to download them, the file is called `gitlab-recovery-codes.txt`.
NOTE:
-Recovery codes are not generated for U2F or WebAuthn devices.
+Recovery codes are not generated for WebAuthn devices.
If you lose the recovery codes, or want to generate new ones, you can use either:
@@ -304,17 +277,6 @@ and you're presented with a second prompt, depending on which type of 2FA you've
When asked, enter the pin from your one time password authenticator's application or a recovery code to sign in.
-### Sign in using a U2F device
-
-To sign in by using a U2F device:
-
-1. Select **Login via U2F Device**.
-1. A light begins blinking on your device. Activate it by touching/pressing
- its button.
-
-A message displays indicating that your device responded to the authentication request, and you're automatically signed
-in.
-
### Sign in using a WebAuthn device
In supported browsers, you should be automatically prompted to activate your WebAuthn device (for example, by touching
@@ -333,7 +295,7 @@ To disable 2FA:
1. Under **Register Two-Factor Authenticator**, enter your current password and select **Disable two-factor
authentication**.
-This clears all your 2FA registrations, including mobile applications and U2F or WebAuthn devices.
+This clears all your 2FA registrations, including mobile applications and WebAuthn devices.
## Recovery options
@@ -414,16 +376,16 @@ a GitLab global administrator disable 2FA for your account:
- Take care that 2FA keeps working after [restoring a GitLab backup](../../../raketasks/backup_restore.md).
- To ensure 2FA authorizes correctly with a time-based one time passwords (TOTP) server, synchronize your GitLab
server's time using a service like NTP. Otherwise, authorization can always fail because of time differences.
-- The GitLab U2F and WebAuthn implementation does _not_ work when the GitLab instance is accessed from multiple hostnames
- or FQDNs. Each U2F or WebAuthn registration is linked to the _current hostname_ at the time of registration, and
+- The GitLab WebAuthn implementation does _not_ work when the GitLab instance is accessed from multiple hostnames
+ or FQDNs. Each WebAuthn registration is linked to the _current hostname_ at the time of registration, and
cannot be used for other hostnames or FQDNs.
For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
- - The user signs in by using `first.host.xyz` and registers their U2F key.
- - The user signs out and attempts to sign in by using `first.host.xyz` - U2F authentication succeeds.
- - The user signs out and attempts to sign in by using `second.host.xyz` - U2F authentication fails, because
- the U2F key has only been registered on `first.host.xyz`.
+ - The user signs in by using `first.host.xyz` and registers their WebAuthn key.
+ - The user signs out and attempts to sign in by using `first.host.xyz` - WebAuthn authentication succeeds.
+ - The user signs out and attempts to sign in by using `second.host.xyz` - WebAuthn authentication fails, because
+ the WebAuthn key has only been registered on `first.host.xyz`.
- To enforce 2FA at the system or group levels see, [Enforce two-factor authentication](../../../security/two_factor_authentication.md).
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 45168bafb38..d52f119ba09 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -34,9 +34,13 @@ If you do not want to update the namespace, you can create a new user or group a
Prerequisites:
-- Your namespace cannot contain a project with [Container Registry](../packages/container_registry/index.md) tags.
-- Your namespace cannot have a project that hosts [GitLab Pages](../project/pages/index.md). For more information,
- see [this procedure in the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#change-your-username-at-gitlabcom).
+- Your namespace must not:
+ - Contain a project with [Container Registry](../packages/container_registry/index.md) tags.
+ - Have a project that hosts [GitLab Pages](../project/pages/index.md). For more information,
+ see [changing your username in the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#change-your-username-at-gitlabcom).
+- Your username must be between 2 and 255 characters in length, and must not:
+ - Contain special characters or emojis.
+ - End with `.<reserved file extension>`, for example `jon.png`. However, `jonpng` is valid.
To change your username:
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index aabc9c5dff1..a20a699e550 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -34,6 +34,8 @@ To report abuse from a user's profile page:
## Report abuse from a user's comment
+> Reporting abuse from comments in epics [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389992) in GitLab 15.10.
+
To report abuse from a user's comment:
1. In the comment, in the upper-right corner, select **More actions** (**{ellipsis_v}**).
diff --git a/doc/user/ssh.md b/doc/user/ssh.md
index d44e6a0e071..7b6fa66b1d7 100644
--- a/doc/user/ssh.md
+++ b/doc/user/ssh.md
@@ -208,7 +208,7 @@ the following command:
ssh-keygen -o -t rsa -b 4096 -C "<comment>"
```
-## Generate an SSH key pair for a FIDO/U2F hardware security key
+## Generate an SSH key pair for a FIDO2 hardware security key
To generate ED25519_SK or ECDSA_SK SSH keys, you must use OpenSSH 8.2 or later:
@@ -548,7 +548,7 @@ If you receive this error, restart your terminal and try the command again.
### `Key enrollment failed: invalid format` error
-You may receive the following error when [generating an SSH key pair for a FIDO/U2F hardware security key](#generate-an-ssh-key-pair-for-a-fidou2f-hardware-security-key):
+You may receive the following error when [generating an SSH key pair for a FIDO2 hardware security key](#generate-an-ssh-key-pair-for-a-fido2-hardware-security-key):
```shell
Key enrollment failed: invalid format
@@ -557,7 +557,7 @@ Key enrollment failed: invalid format
You can troubleshoot this by trying the following:
- Run the `ssh-keygen` command using `sudo`.
-- Verify your IDO/U2F hardware security key supports
+- Verify your FIDO2 hardware security key supports
the key type provided.
- Verify the version of OpenSSH is 8.2 or greater by
running `ssh -V`.
diff --git a/lefthook.yml b/lefthook.yml
index 39496e0d241..6a80713450f 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -86,6 +86,12 @@ pre-push:
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: 'data/removals/*.yml'
run: echo "Changes to removals files detected. Checking removals..\n"; bundle exec rake gitlab:docs:check_removals
+ db-schema-changes:
+ tags: database
+ files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
+ glob: 'db/structure.sql'
+ run: scripts/validate_schema_changes
+
scripts:
"merge_conflicts":
skip: true # This is disabled by default. You can enable this check by adding skip: false in lefhook-local.yml https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#skip
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index f26b3a1d8c2..ef4aec44cb5 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -163,8 +163,13 @@ module API
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
- packages = ::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
- .execute
+ packages =
+ if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ finder_for_endpoint_scope(package_name).execute
+ else
+ ::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
+ .execute
+ end
redirect_request = project_or_nil.blank? || packages.empty?
diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb
index 352d77f472c..4eb6c39b7dc 100644
--- a/lib/api/helpers/packages/npm.rb
+++ b/lib/api/helpers/packages/npm.rb
@@ -33,6 +33,15 @@ module API
end
end
+ def finder_for_endpoint_scope(package_name)
+ case endpoint_scope
+ when :project
+ ::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
+ when :instance
+ ::Packages::Npm::PackageFinder.new(package_name, namespace: top_namespace_from(package_name))
+ end
+ end
+
def project_or_nil
# mainly used by the metadata endpoint where we need to get a project
# and return nil if not found (no errors should be raised)
@@ -50,11 +59,17 @@ module API
params[:id]
when :instance
package_name = params[:package_name]
- namespace_path = ::Packages::Npm.scope_of(package_name)
- next unless namespace_path
- namespace = Namespace.top_most
- .by_path(namespace_path)
+ namespace =
+ if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ top_namespace_from(package_name)
+ else
+ namespace_path = ::Packages::Npm.scope_of(package_name)
+ next unless namespace_path
+
+ Namespace.top_most.by_path(namespace_path)
+ end
+
next unless namespace
finder = ::Packages::Npm::PackageFinder.new(
@@ -67,6 +82,15 @@ module API
end
end
end
+
+ private
+
+ def top_namespace_from(package_name)
+ namespace_path = ::Packages::Npm.scope_of(package_name)
+ return unless namespace_path
+
+ Namespace.top_most.by_path(namespace_path)
+ end
end
end
end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index f6c4e94a064..534b84afc23 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -119,8 +119,7 @@ module Gitlab
def expand_config(config)
build_config(config)
- rescue Gitlab::Config::Loader::Yaml::DataTooLargeError,
- Gitlab::Config::Loader::MultiDocYaml::DataTooLargeError => e
+ rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
track_and_raise_for_dev_exception(e)
raise Config::ConfigError, e.message
diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb
index 51ca3ea0683..ef2e2d8cccc 100644
--- a/lib/gitlab/ci/config/external/file/base.rb
+++ b/lib/gitlab/ci/config/external/file/base.rb
@@ -115,7 +115,7 @@ module Gitlab
def content_hash
strong_memoize(:content_hash) do
- ::Gitlab::Ci::Config::Yaml.load!(content)
+ ::Gitlab::Ci::Config::Yaml.load!(content, project: context.project)
end
rescue Gitlab::Config::Loader::FormatError
nil
diff --git a/lib/gitlab/ci/config/yaml.rb b/lib/gitlab/ci/config/yaml.rb
index 94ef0afe7f9..31efe6ab6d9 100644
--- a/lib/gitlab/ci/config/yaml.rb
+++ b/lib/gitlab/ci/config/yaml.rb
@@ -7,11 +7,16 @@ module Gitlab
AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
MAX_DOCUMENTS = 2
- class << self
- def load!(content)
+ class Loader
+ def initialize(content, project: nil)
+ @content = content
+ @project = project
+ end
+
+ def load!
ensure_custom_tags
- if ::Feature.enabled?(:ci_multi_doc_yaml)
+ if project.present? && ::Feature.enabled?(:ci_multi_doc_yaml, project)
Gitlab::Config::Loader::MultiDocYaml.new(
content,
max_documents: MAX_DOCUMENTS,
@@ -24,6 +29,8 @@ module Gitlab
private
+ attr_reader :content, :project
+
def ensure_custom_tags
@ensure_custom_tags ||= begin
AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
@@ -32,6 +39,12 @@ module Gitlab
end
end
end
+
+ class << self
+ def load!(content, project: nil)
+ Loader.new(content, project: project).load!
+ end
+ end
end
end
end
diff --git a/lib/gitlab/config/loader/multi_doc_yaml.rb b/lib/gitlab/config/loader/multi_doc_yaml.rb
index 346adc79896..34080d26b7c 100644
--- a/lib/gitlab/config/loader/multi_doc_yaml.rb
+++ b/lib/gitlab/config/loader/multi_doc_yaml.rb
@@ -4,59 +4,48 @@ module Gitlab
module Config
module Loader
class MultiDocYaml
- TooManyDocumentsError = Class.new(Loader::FormatError)
- DataTooLargeError = Class.new(Loader::FormatError)
- NotHashError = Class.new(Loader::FormatError)
+ include Gitlab::Utils::StrongMemoize
- MULTI_DOC_DIVIDER = /^---$/.freeze
+ MULTI_DOC_DIVIDER = /^---\s+/.freeze
def initialize(config, max_documents:, additional_permitted_classes: [])
+ @config = config
@max_documents = max_documents
- @safe_config = load_config(config, additional_permitted_classes)
+ @additional_permitted_classes = additional_permitted_classes
end
- def load!
- raise TooManyDocumentsError, 'The parsed YAML has too many documents' if too_many_documents?
- raise DataTooLargeError, 'The parsed YAML is too big' if too_big?
- raise NotHashError, 'Invalid configuration format' unless all_hashes?
-
- safe_config.map(&:deep_symbolize_keys)
+ def valid?
+ documents.all?(&:valid?)
end
- private
-
- attr_reader :safe_config, :max_documents
-
- def load_config(config, additional_permitted_classes)
- config.split(MULTI_DOC_DIVIDER).filter_map do |document|
- YAML.safe_load(document,
- permitted_classes: [Symbol, *additional_permitted_classes],
- permitted_symbols: [],
- aliases: true
- )
- end
- rescue Psych::Exception => e
- raise Loader::FormatError, e.message
+ def load_raw!
+ documents.map(&:load_raw!)
end
- def all_hashes?
- safe_config.all?(Hash)
+ def load!
+ documents.map(&:load!)
end
- def too_many_documents?
- safe_config.count > max_documents
- end
+ private
+
+ attr_reader :config, :max_documents, :additional_permitted_classes
+
+ # Valid YAML files can start with either a leading delimiter or no delimiter.
+ # To avoid counting a leading delimiter towards the document limit,
+ # this method splits the file by one more than the maximum number of permitted documents.
+ # It then discards the first document if it is blank.
+ def documents
+ docs = config
+ .split(MULTI_DOC_DIVIDER, max_documents_including_leading_delimiter)
+ .map { |d| Yaml.new(d, additional_permitted_classes: additional_permitted_classes) }
- def too_big?
- !deep_sizes.all?(&:valid?)
+ docs.shift if docs.first.blank?
+ docs
end
+ strong_memoize_attr :documents
- def deep_sizes
- safe_config.map do |config|
- Gitlab::Utils::DeepSize.new(config,
- max_size: Gitlab::CurrentSettings.current_application_settings.max_yaml_size_bytes,
- max_depth: Gitlab::CurrentSettings.current_application_settings.max_yaml_depth)
- end
+ def max_documents_including_leading_delimiter
+ max_documents + 1
end
end
end
diff --git a/lib/gitlab/config/loader/yaml.rb b/lib/gitlab/config/loader/yaml.rb
index 7b87b5b8f97..38f8eca3c3c 100644
--- a/lib/gitlab/config/loader/yaml.rb
+++ b/lib/gitlab/config/loader/yaml.rb
@@ -34,6 +34,10 @@ module Gitlab
@symbolized_config ||= load_raw!.deep_symbolize_keys
end
+ def blank?
+ @config.blank?
+ end
+
private
def hash?
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 25d6277a2be..f5c1942a263 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -64,7 +64,6 @@ namespace :tw do
CodeOwnerRule.new('Provision', '@fneill'),
CodeOwnerRule.new('Purchase', '@fneill'),
CodeOwnerRule.new('Redirect', 'Redirect'),
- CodeOwnerRule.new('Release', '@rdickenson'),
CodeOwnerRule.new('Respond', '@msedlakjakubowski'),
CodeOwnerRule.new('Runner', '@fneill'),
CodeOwnerRule.new('Runner SaaS', '@fneill'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fc126de2e55..46a857a2db6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -28748,10 +28748,7 @@ msgstr ""
msgid "NorthstarNavigation|Toggle new navigation"
msgstr ""
-msgid "Not all browsers support U2F devices. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even when you're using an unsupported browser."
-msgstr ""
-
-msgid "Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even from an unsupported browser."
+msgid "Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in, even from an unsupported browser."
msgstr ""
msgid "Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser."
@@ -35601,9 +35598,6 @@ msgstr ""
msgid "Register a one-time password authenticator or a WebAuthn device first."
msgstr ""
-msgid "Register a universal two-factor (U2F) device"
-msgstr ""
-
msgid "Register device"
msgstr ""
@@ -44529,10 +44523,7 @@ msgstr ""
msgid "This vulnerability was automatically resolved because its vulnerability type was disabled in this project or removed from GitLab's default ruleset."
msgstr ""
-msgid "This will invalidate your registered applications and U2F / WebAuthn devices."
-msgstr ""
-
-msgid "This will invalidate your registered applications and U2F devices."
+msgid "This will invalidate your registered applications and WebAuthn devices."
msgstr ""
msgid "This will remove the fork relationship between this project and %{fork_source}."
@@ -45712,12 +45703,6 @@ msgstr ""
msgid "Type to search"
msgstr ""
-msgid "U2F Devices (%{length})"
-msgstr ""
-
-msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
-msgstr ""
-
msgid "URL"
msgstr ""
@@ -49457,9 +49442,6 @@ msgstr ""
msgid "You do not have permissions to run the import."
msgstr ""
-msgid "You don't have any U2F devices registered yet."
-msgstr ""
-
msgid "You don't have any WebAuthn devices registered yet."
msgstr ""
@@ -49941,9 +49923,6 @@ msgstr ""
msgid "Your browser does not support iFrames"
msgstr ""
-msgid "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer)."
-msgstr ""
-
msgid "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+)."
msgstr ""
diff --git a/scripts/database/schema_validator.rb b/scripts/database/schema_validator.rb
new file mode 100644
index 00000000000..11a53faa945
--- /dev/null
+++ b/scripts/database/schema_validator.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative '../migration_schema_validator'
+
+class SchemaValidator < MigrationSchemaValidator
+ ALLOW_SCHEMA_CHANGES = 'ALLOW_SCHEMA_CHANGES'
+ COMMIT_MESSAGE_SKIP_TAG = 'skip-db-structure-check'
+
+ def validate!
+ return if should_skip?
+
+ return if schema_changes.empty?
+
+ die "#{FILENAME} was changed, and no migrations were added:\n#{schema_changes}" if committed_migrations.empty?
+ end
+
+ private
+
+ def schema_changes
+ @schema_changes ||= run("git diff #{diff_target} HEAD -- #{FILENAME}")
+ end
+
+ def should_skip?
+ skip_env_present? || skip_commit_present?
+ end
+
+ def skip_env_present?
+ !ENV[ALLOW_SCHEMA_CHANGES].to_s.empty?
+ end
+
+ def skip_commit_present?
+ run("git show -s --format=%B -n 1").to_s.include?(COMMIT_MESSAGE_SKIP_TAG)
+ end
+end
diff --git a/scripts/validate_schema_changes b/scripts/validate_schema_changes
new file mode 100755
index 00000000000..a6a01a060ce
--- /dev/null
+++ b/scripts/validate_schema_changes
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+# frozen_string_literal: true
+
+require_relative './database/schema_validator'
+
+SchemaValidator.new.validate!
diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb
index 5fa7a7f278d..e0890f4d160 100644
--- a/spec/controllers/admin/sessions_controller_spec.rb
+++ b/spec/controllers/admin/sessions_controller_spec.rb
@@ -268,16 +268,6 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
end
end
- context 'when using two-factor authentication via U2F' do
- it_behaves_like 'when using two-factor authentication via hardware device' do
- let(:user) { create(:admin, :two_factor_via_u2f) }
-
- before do
- stub_feature_flags(webauthn: false)
- end
- end
- end
-
context 'when using two-factor authentication via WebAuthn' do
it_behaves_like 'when using two-factor authentication via hardware device' do
let(:user) { create(:admin, :two_factor_via_webauthn) }
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 1f8845a55bf..be5dd4f961a 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -109,6 +109,41 @@ RSpec.describe GraphqlController, feature_category: :integrations do
])
end
+ it 'executes a multiplexed queries with variables with no errors' do
+ query = <<~GQL
+ mutation($a: String!, $b: String!) {
+ echoCreate(input: { messages: [$a, $b] }) { echoes }
+ }
+ GQL
+ multiplex = [
+ { query: query, variables: { a: 'A', b: 'B' } },
+ { query: query, variables: { a: 'a', b: 'b' } }
+ ]
+
+ post :execute, params: { _json: multiplex }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(
+ [
+ { 'data' => { 'echoCreate' => { 'echoes' => %w[A B] } } },
+ { 'data' => { 'echoCreate' => { 'echoes' => %w[a b] } } }
+ ])
+ end
+
+ it 'does not allow string as _json parameter' do
+ post :execute, params: { _json: 'bad' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ "errors" => [
+ {
+ "message" => "Unexpected end of document",
+ "locations" => []
+ }
+ ]
+ })
+ end
+
it 'sets a limit on the total query size' do
graphql_query = "{#{(['__typename'] * 1000).join(' ')}}"
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index 80e2dcd5174..a6c980f539c 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -56,6 +56,14 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
expect(sidebar).to have_link _('Packages and registries')
end
+ it 'passes axe automated accessibility testing', :js do
+ visit_settings_page
+
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within '[data-testid="packages-and-registries-group-settings"]'
+ end
+
it 'has a Duplicate packages section', :js do
visit_settings_page
diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
index d4c1fe4d43e..57aa3a56c6d 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -38,6 +38,15 @@ feature_category: :projects do
expect(section).to have_text 'Clean up image tags'
end
+ it 'passes axe automated accessibility testing' do
+ subject
+
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within('[data-testid="container-expiration-policy-project-settings"]')
+ .skipping :'link-in-text-block'
+ end
+
it 'saves cleanup policy submit the form' do
subject
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 072b5f7f3b0..628fa23afdc 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -21,6 +21,15 @@ feature_category: :projects do
end
context 'as owner', :js do
+ it 'passes axe automated accessibility testing' do
+ subject
+
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-project-settings"]')
+ .skipping :'link-in-text-block'
+ end
+
it 'shows active tab on sidebar' do
subject
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
deleted file mode 100644
index 86af1970909..00000000000
--- a/spec/features/u2f_spec.rb
+++ /dev/null
@@ -1,216 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js,
-feature_category: :system_access do
- include Spec::Support::Helpers::Features::TwoFactorHelpers
-
- before do
- stub_feature_flags(webauthn: false)
- end
-
- it_behaves_like 'hardware device for 2fa', 'U2F'
-
- describe "registration" do
- let(:user) { create(:user) }
-
- before do
- gitlab_sign_in(user)
- user.update_attribute(:otp_required_for_login, true)
- end
-
- describe 'when 2FA via OTP is enabled' do
- it 'allows registering more than one device' do
- visit profile_account_path
-
- # First device
- manage_two_factor_authentication
- first_device = register_u2f_device
- expect(page).to have_content('Your U2F device was registered')
-
- # Second device
- second_device = register_u2f_device(name: 'My other device')
- expect(page).to have_content('Your U2F device was registered')
-
- expect(page).to have_content(first_device.name)
- expect(page).to have_content(second_device.name)
- expect(U2fRegistration.count).to eq(2)
- end
- end
-
- it 'allows the same device to be registered for multiple users' do
- # U2f specs will be removed after WebAuthn migration completed
- pending('FakeU2fDevice has static key handle, '\
- 'leading to duplicate credential_xid for WebAuthn during migration, '\
- 'resulting in unique constraint violation')
-
- # First user
- visit profile_account_path
- manage_two_factor_authentication
- u2f_device = register_u2f_device
- expect(page).to have_content('Your U2F device was registered')
- gitlab_sign_out
-
- # Second user
- user = gitlab_sign_in(:user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_u2f_device(u2f_device, name: 'My other device')
- expect(page).to have_content('Your U2F device was registered')
-
- expect(U2fRegistration.count).to eq(2)
- end
-
- context "when there are form errors" do
- it "doesn't register the device if there are errors" do
- visit profile_account_path
- manage_two_factor_authentication
-
- # Have the "u2f device" respond with bad data
- page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- click_on 'Register device'
-
- expect(U2fRegistration.count).to eq(0)
- expect(page).to have_content("The form contains the following error")
- expect(page).to have_content("did not send a valid JSON response")
- end
-
- it "allows retrying registration" do
- visit profile_account_path
- manage_two_factor_authentication
-
- # Failed registration
- page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- click_on 'Register device'
- expect(page).to have_content("The form contains the following error")
-
- # Successful registration
- register_u2f_device
-
- expect(page).to have_content('Your U2F device was registered')
- expect(U2fRegistration.count).to eq(1)
- end
- end
- end
-
- describe "authentication" do
- let(:user) { create(:user) }
-
- before do
- # Register and logout
- gitlab_sign_in(user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- @u2f_device = register_u2f_device
- gitlab_sign_out
- end
-
- describe "when 2FA via OTP is disabled" do
- it "allows logging in with the U2F device" do
- user.update_attribute(:otp_required_for_login, false)
- gitlab_sign_in(user)
-
- @u2f_device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
- end
-
- describe "when 2FA via OTP is enabled" do
- it "allows logging in with the U2F device" do
- user.update_attribute(:otp_required_for_login, true)
- gitlab_sign_in(user)
-
- @u2f_device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
- end
-
- describe "when a given U2F device has already been registered by another user" do
- describe "but not the current user" do
- it "does not allow logging in with that particular device" do
- # Register current user with the different U2F device
- current_user = gitlab_sign_in(:user)
- current_user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_u2f_device(name: 'My other device')
- gitlab_sign_out
-
- # Try authenticating user with the old U2F device
- gitlab_sign_in(current_user)
- @u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('Authentication via U2F device failed')
- end
- end
-
- describe "and also the current user" do
- it "allows logging in with that particular device" do
- # U2f specs will be removed after WebAuthn migration completed
- pending('FakeU2fDevice has static key handle, '\
- 'leading to duplicate credential_xid for WebAuthn during migration, '\
- 'resulting in unique constraint violation')
-
- # Register current user with the same U2F device
- current_user = gitlab_sign_in(:user)
- current_user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_u2f_device(@u2f_device)
- gitlab_sign_out
-
- # Try authenticating user with the same U2F device
- gitlab_sign_in(current_user)
- @u2f_device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
- end
- end
-
- describe "when a given U2F device has not been registered" do
- it "does not allow logging in with that particular device" do
- unregistered_device = FakeU2fDevice.new(page, 'My device')
- gitlab_sign_in(user)
- unregistered_device.respond_to_u2f_authentication
-
- expect(page).to have_content('Authentication via U2F device failed')
- end
- end
-
- describe "when more than one device has been registered by the same user" do
- it "allows logging in with either device" do
- # Register first device
- user = gitlab_sign_in(:user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_two_factor_auth_path
- expect(page).to have_content("Your device needs to be set up.")
- first_device = register_u2f_device
-
- # Register second device
- visit profile_two_factor_auth_path
- expect(page).to have_content("Your device needs to be set up.")
- second_device = register_u2f_device(name: 'My other device')
- gitlab_sign_out
-
- # Authenticate as both devices
- [first_device, second_device].each do |device|
- gitlab_sign_in(user)
- device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
-
- gitlab_sign_out
- end
- end
- end
- end
-end
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
deleted file mode 100644
index 3ae7fcf1c49..00000000000
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import U2FAuthenticate from '~/authentication/u2f/authenticate';
-import 'vendor/u2f';
-import MockU2FDevice from './mock_u2f_device';
-
-describe('U2FAuthenticate', () => {
- let u2fDevice;
- let container;
- let component;
-
- beforeEach(() => {
- loadHTMLFixture('u2f/authenticate.html');
- u2fDevice = new MockU2FDevice();
- container = $('#js-authenticate-token-2fa');
- component = new U2FAuthenticate(
- container,
- '#js-login-token-2fa-form',
- {
- sign_requests: [],
- },
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form'),
- );
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('with u2f unavailable', () => {
- let oldu2f;
-
- beforeEach(() => {
- jest.spyOn(component, 'switchToFallbackUI').mockImplementation(() => {});
- oldu2f = window.u2f;
- window.u2f = null;
- });
-
- afterEach(() => {
- window.u2f = oldu2f;
- });
-
- it('falls back to normal 2fa', async () => {
- await component.start();
- expect(component.switchToFallbackUI).toHaveBeenCalled();
- });
- });
-
- describe('with u2f available', () => {
- beforeEach(() => {
- // bypass automatic form submission within renderAuthenticated
- jest.spyOn(component, 'renderAuthenticated').mockReturnValue(true);
- u2fDevice = new MockU2FDevice();
-
- return component.start();
- });
-
- it('allows authenticating via a U2F device', () => {
- const inProgressMessage = container.find('p');
-
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
- });
-
- expect(component.renderAuthenticated).toHaveBeenCalledWith(
- '{"deviceData":"this is data from the device"}',
- );
- });
-
- describe('errors', () => {
- it('displays an error message', () => {
- const setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('There was a problem communicating with your device');
- });
-
- it('allows retrying authentication after an error', () => {
- let setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const retryButton = container.find('#js-token-2fa-try-again');
- retryButton.trigger('click');
- setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
- });
-
- expect(component.renderAuthenticated).toHaveBeenCalledWith(
- '{"deviceData":"this is data from the device"}',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/authentication/u2f/mock_u2f_device.js b/spec/frontend/authentication/u2f/mock_u2f_device.js
deleted file mode 100644
index ec8425a4e3e..00000000000
--- a/spec/frontend/authentication/u2f/mock_u2f_device.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-unused-expressions */
-
-export default class MockU2FDevice {
- constructor() {
- this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
- this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
- window.u2f || (window.u2f = {});
- window.u2f.register = (appId, registerRequests, signRequests, callback) => {
- this.registerCallback = callback;
- };
- window.u2f.sign = (appId, challenges, signRequests, callback) => {
- this.authenticateCallback = callback;
- };
- }
-
- respondToRegisterRequest(params) {
- return this.registerCallback(params);
- }
-
- respondToAuthenticateRequest(params) {
- return this.authenticateCallback(params);
- }
-}
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
deleted file mode 100644
index 23d1e5c7dee..00000000000
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { trimText } from 'helpers/text_helper';
-import U2FRegister from '~/authentication/u2f/register';
-import 'vendor/u2f';
-import MockU2FDevice from './mock_u2f_device';
-
-describe('U2FRegister', () => {
- let u2fDevice;
- let container;
- let component;
-
- beforeEach(() => {
- loadHTMLFixture('u2f/register.html');
- u2fDevice = new MockU2FDevice();
- container = $('#js-register-token-2fa');
- component = new U2FRegister(container, {});
- return component.start();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('allows registering a U2F device', () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
-
- expect(trimText(setupButton.text())).toBe('Set up new device');
- setupButton.trigger('click');
- const inProgressMessage = container.children('p');
-
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- u2fDevice.respondToRegisterRequest({
- deviceData: 'this is data from the device',
- });
- const registeredMessage = container.find('p');
- const deviceResponse = container.find('#js-device-response');
-
- expect(registeredMessage.text()).toContain('Your device was successfully set up!');
- expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
- });
-
- describe('errors', () => {
- it("doesn't allow the same device to be registered twice (for the same user", () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 4,
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('already been registered with us');
- });
-
- it('displays an error message for other errors', () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 'error!',
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('There was a problem communicating with your device');
- });
-
- it('allows retrying registration after an error', () => {
- let setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 'error!',
- });
- const retryButton = container.find('#js-token-2fa-try-again');
- retryButton.trigger('click');
- setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- deviceData: 'this is data from the device',
- });
- const registeredMessage = container.find('p');
-
- expect(registeredMessage.text()).toContain('Your device was successfully set up!');
- });
- });
-});
diff --git a/spec/frontend/authentication/u2f/util_spec.js b/spec/frontend/authentication/u2f/util_spec.js
deleted file mode 100644
index 67fd4c73243..00000000000
--- a/spec/frontend/authentication/u2f/util_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { canInjectU2fApi } from '~/authentication/u2f/util';
-
-describe('U2F Utils', () => {
- describe('canInjectU2fApi', () => {
- it('returns false for Chrome < 41', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns true for Chrome >= 41', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(true);
- });
-
- it('returns false for Opera < 40', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns true for Opera >= 40', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991';
-
- expect(canInjectU2fApi(userAgent)).toBe(true);
- });
-
- it('returns false for Safari', () => {
- const userAgent =
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Chrome on Android', () => {
- const userAgent =
- 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Chrome on iOS', () => {
- const userAgent =
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Safari on iOS', () => {
- const userAgent =
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
deleted file mode 100644
index 96820c9ae80..00000000000
--- a/spec/frontend/fixtures/u2f.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.context 'U2F' do
- include JavaScriptFixturesHelpers
-
- let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') }
-
- before do
- stub_feature_flags(webauthn: false)
- end
-
- describe SessionsController, '(JavaScript fixtures)', type: :controller do
- include DeviseHelpers
-
- render_views
-
- before do
- set_devise_mapping(context: @request)
- end
-
- it 'u2f/authenticate.html' do
- allow(controller).to receive(:find_user).and_return(user)
-
- post :create, params: { user: { login: user.username, password: user.password } }
-
- expect(response).to be_successful
- end
- end
-
- describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do
- render_views
-
- before do
- sign_in(user)
- allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
- allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
- end
- end
-
- it 'u2f/register.html' do
- get :show
-
- expect(response).to be_successful
- end
- end
-end
diff --git a/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
index 192714b657e..f63aacecce6 100644
--- a/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
@@ -6,22 +6,117 @@ RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline
let(:loader) { described_class.new(yml, max_documents: 2) }
describe '#load!' do
- let(:yml) do
- <<~YAML
- spec:
- inputs:
- test_input:
- ---
- test_job:
- script: echo "$[[ inputs.test_input ]]"
- YAML
+ context 'when a simple single delimiter is being used' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ env:
+ ---
+ test:
+ script: echo "$[[ inputs.env ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to contain_exactly(
+ { spec: { inputs: { env: nil } } },
+ { test: { script: 'echo "$[[ inputs.env ]]"' } }
+ )
+ end
+ end
+
+ context 'when the delimiter has a trailing configuration' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ test_input:
+ --- !test/content
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to contain_exactly(
+ { spec: { inputs: { test_input: nil } } },
+ { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
+ )
+ end
+ end
+
+ context 'when the YAML file has a leading delimiter' do
+ let(:yml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ --- !test/content
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to contain_exactly(
+ { spec: { inputs: { test_input: nil } } },
+ { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
+ )
+ end
+ end
+
+ context 'when the delimiter is followed by content on the same line' do
+ let(:yml) do
+ <<~YAML
+ --- a: 1
+ --- b: 2
+ YAML
+ end
+
+ it 'loads the content as part of the document' do
+ expect(loader.load!).to contain_exactly({ a: 1 }, { b: 2 })
+ end
end
- it 'returns the loaded YAML with all keys as symbols' do
- expect(loader.load!).to eq([
- { spec: { inputs: { test_input: nil } } },
- { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
- ])
+ context 'when the delimiter does not have trailing whitespace' do
+ let(:yml) do
+ <<~YAML
+ --- a: 1
+ ---b: 2
+ YAML
+ end
+
+ it 'is not a valid delimiter' do
+ expect(loader.load!).to contain_exactly({ :'---b' => 2, a: 1 }) # rubocop:disable Style/HashSyntax
+ end
+ end
+
+ context 'when the YAML file has whitespace preceding the content' do
+ let(:yml) do
+ <<-EOYML
+ variables:
+ SUPPORTED: "parsed"
+
+ workflow:
+ rules:
+ - if: $VAR == "value"
+
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'loads everything correctly' do
+ expect(loader.load!).to contain_exactly(
+ {
+ variables: { SUPPORTED: 'parsed' },
+ workflow: { rules: [{ if: '$VAR == "value"' }] },
+ hello: { script: 'echo world' }
+ }
+ )
+ end
end
context 'when the YAML file is empty' do
@@ -32,67 +127,68 @@ RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline
end
end
- context 'when the parsed YAML is too big' do
+ context 'when there are more than the maximum number of documents' do
let(:yml) do
<<~YAML
- a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
- b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
- c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
- d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
- e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
- f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
- g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
- h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
- i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
- ---
- a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
- b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
- c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
- d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
- e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
- f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
- g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
- h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
- i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
+ --- a: 1
+ --- b: 2
+ --- c: 3
+ --- d: 4
YAML
end
- it 'raises a DataTooLargeError' do
- expect { loader.load! }.to raise_error(described_class::DataTooLargeError, 'The parsed YAML is too big')
+ it 'stops splitting documents after the maximum number' do
+ expect(loader.load!).to contain_exactly({ a: 1 }, { b: 2 })
end
end
+ end
+
+ describe '#load_raw!' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ test_input:
+ --- !test/content
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as strings' do
+ expect(loader.load_raw!).to contain_exactly(
+ { 'spec' => { 'inputs' => { 'test_input' => nil } } },
+ { 'test_job' => { 'script' => 'echo "$[[ inputs.test_input ]]"' } }
+ )
+ end
+ end
- context 'when a document is not a hash' do
+ describe '#valid?' do
+ context 'when a document is invalid' do
let(:yml) do
<<~YAML
- not_a_hash
+ a: b
---
- test_job:
- script: echo "$[[ inputs.test_input ]]"
+ c
YAML
end
- it 'raises a NotHashError' do
- expect { loader.load! }.to raise_error(described_class::NotHashError, 'Invalid configuration format')
+ it 'returns false' do
+ expect(loader).not_to be_valid
end
end
- context 'when there are too many documents' do
+ context 'when the number of documents is below the maximum and all documents are valid' do
let(:yml) do
<<~YAML
a: b
---
c: d
- ---
- e: f
YAML
end
- it 'raises a TooManyDocumentsError' do
- expect { loader.load! }.to raise_error(
- described_class::TooManyDocumentsError,
- 'The parsed YAML has too many documents'
- )
+ it 'returns true' do
+ expect(loader).to be_valid
end
end
end
diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index 6d0ed1fc2f8..bba66f33718 100644
--- a/spec/lib/gitlab/config/loader/yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -182,4 +182,30 @@ RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_composi
)
end
end
+
+ describe '#blank?' do
+ context 'when the loaded YAML is empty' do
+ let(:yml) do
+ <<~YAML
+ # only comments here
+ YAML
+ end
+
+ it 'returns true' do
+ expect(loader).to be_blank
+ end
+ end
+
+ context 'when the loaded YAML has content' do
+ let(:yml) do
+ <<~YAML
+ test: value
+ YAML
+ end
+
+ it 'returns false' do
+ expect(loader).not_to be_blank
+ end
+ end
+ end
end
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index dcd2e4ae677..591a8ee68dc 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -11,8 +11,38 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests', scope: :instance do
- let(:url) { api("/packages/npm/#{package_name}") }
+ let(:url) { api("/packages/npm/#{package_name}") }
+
+ it_behaves_like 'handling get metadata requests', scope: :instance
+
+ context 'with a duplicate package name in another project' do
+ subject { get(url) }
+
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) do
+ create(:npm_package,
+ project: project2,
+ name: "@#{group.path}/scoped_package",
+ version: '1.2.0')
+ end
+
+ it 'includes all matching package versions in the response' do
+ subject
+
+ expect(json_response['versions'].keys).to match_array([package.version, package2.version])
+ end
+
+ context 'with the feature flag disabled' do
+ before do
+ stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
+ end
+
+ it 'returns matching package versions from only one project' do
+ subject
+
+ expect(json_response['versions'].keys).to match_array([package2.version])
+ end
+ end
end
end
diff --git a/spec/scripts/database/schema_validator_spec.rb b/spec/scripts/database/schema_validator_spec.rb
new file mode 100644
index 00000000000..13be8e291da
--- /dev/null
+++ b/spec/scripts/database/schema_validator_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../scripts/database/schema_validator'
+
+RSpec.describe SchemaValidator, feature_category: :database do
+ subject(:validator) { described_class.new }
+
+ describe "#validate!" do
+ before do
+ allow(validator).to receive(:committed_migrations).and_return(committed_migrations)
+ allow(validator).to receive(:run).and_return(schema_changes)
+ end
+
+ context 'when schema changes are introduced without migrations' do
+ let(:committed_migrations) { [] }
+ let(:schema_changes) { 'db/structure.sql' }
+
+ it 'terminates the execution' do
+ expect { validator.validate! }.to raise_error(SystemExit)
+ end
+ end
+
+ context 'when schema changes are introduced with migrations' do
+ let(:committed_migrations) { ['20211006103122_my_migration.rb'] }
+ let(:schema_changes) { 'db/structure.sql' }
+ let(:command) { 'git diff db/structure.sql -- db/structure.sql' }
+ let(:base_message) { 'db/structure.sql was changed, and no migrations were added' }
+
+ before do
+ allow(validator).to receive(:die)
+ end
+
+ it 'skips schema validations' do
+ expect(validator.validate!).to be_nil
+ end
+ end
+
+ context 'when skipping validations through ENV variable' do
+ let(:committed_migrations) { [] }
+ let(:schema_changes) { 'db/structure.sql' }
+
+ before do
+ stub_env('ALLOW_SCHEMA_CHANGES', true)
+ end
+
+ it 'skips schema validations' do
+ expect(validator.validate!).to be_nil
+ end
+ end
+
+ context 'when skipping validations through commit message' do
+ let(:committed_migrations) { [] }
+ let(:schema_changes) { 'db/structure.sql' }
+ let(:commit_message) { "Changes db/strucure.sql file\nskip-db-structure-check" }
+
+ before do
+ allow(validator).to receive(:run).and_return(commit_message)
+ end
+
+ it 'skips schema validations' do
+ expect(validator.validate!).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d9afe9c5284..fa16603ff70 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -38,6 +38,7 @@ require 'test_prof/recipes/rspec/let_it_be'
require 'test_prof/factory_default'
require 'test_prof/factory_prof/nate_heckler'
require 'parslet/rig/rspec'
+require 'axe-rspec'
rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
deleted file mode 100644
index 2ed1222ebd3..00000000000
--- a/spec/support/helpers/fake_u2f_device.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-class FakeU2fDevice
- attr_reader :name
-
- def initialize(page, name, device = nil)
- @page = page
- @name = name
- @u2f_device = device
- end
-
- def respond_to_u2f_registration
- app_id = @page.evaluate_script('gon.u2f.app_id')
- challenges = @page.evaluate_script('gon.u2f.challenges')
-
- json_response = u2f_device(app_id).register_response(challenges[0])
-
- @page.execute_script("
- u2f.register = function(appId, registerRequests, signRequests, callback) {
- callback(#{json_response});
- };
- ")
- end
-
- def respond_to_u2f_authentication
- app_id = @page.evaluate_script('gon.u2f.app_id')
- challenge = @page.evaluate_script('gon.u2f.challenge')
- json_response = u2f_device(app_id).sign_response(challenge)
-
- @page.execute_script("
- u2f.sign = function(appId, challenges, signRequests, callback) {
- callback(#{json_response});
- };
- window.gl.u2fAuthenticate.start();
- ")
- end
-
- def fake_u2f_authentication
- @page.execute_script("window.gl.u2fAuthenticate.renderAuthenticated('abc');")
- end
-
- private
-
- def u2f_device(app_id)
- @u2f_device ||= U2F::FakeU2F.new(app_id)
- end
-end
diff --git a/spec/support/helpers/features/two_factor_helpers.rb b/spec/support/helpers/features/two_factor_helpers.rb
index 08a7665201f..824ecddc392 100644
--- a/spec/support/helpers/features/two_factor_helpers.rb
+++ b/spec/support/helpers/features/two_factor_helpers.rb
@@ -20,16 +20,6 @@ module Spec
wait_for_requests
end
- def register_u2f_device(u2f_device = nil, name: 'My device')
- u2f_device ||= FakeU2fDevice.new(page, name)
- u2f_device.respond_to_u2f_registration
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- fill_in "Pick a name", with: name
- click_on 'Register device'
- u2f_device
- end
-
# Registers webauthn device via UI
def register_webauthn_device(webauthn_device = nil, name: 'My device')
webauthn_device ||= FakeWebauthnDevice.new(page, name)
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 5fde80e6dc9..95ad38fbb5b 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -139,11 +139,6 @@ module LoginHelpers
click_link_or_button "oauth-login-#{provider}"
end
- def fake_successful_u2f_authentication
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
- FakeU2fDevice.new(page, nil).fake_u2f_authentication
- end
-
def fake_successful_webauthn_authentication
allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
FakeWebauthnDevice.new(page, nil).fake_webauthn_authentication
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index 44f30c32472..b6339607d6b 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -6,8 +6,6 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
def register_device(device_type, **kwargs)
case device_type.downcase
- when "u2f"
- register_u2f_device(**kwargs)
when "webauthn"
register_webauthn_device(**kwargs)
else
diff --git a/spec/support_specs/helpers/packages/npm_spec.rb b/spec/support_specs/helpers/packages/npm_spec.rb
new file mode 100644
index 00000000000..e1316a10fb1
--- /dev/null
+++ b/spec/support_specs/helpers/packages/npm_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registry do # rubocop: disable RSpec/FilePath
+ let(:object) { klass.new(params) }
+ let(:klass) do
+ Struct.new(:params) do
+ include ::API::Helpers
+ include ::API::Helpers::Packages::Npm
+ end
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:namespace) { group }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package) { create(:npm_package, project: project) }
+
+ describe '#endpoint_scope' do
+ subject { object.endpoint_scope }
+
+ context 'when params includes an id' do
+ let(:params) { { id: 42, package_name: 'foo' } }
+
+ it { is_expected.to eq(:project) }
+ end
+
+ context 'when params does not include an id' do
+ let(:params) { { package_name: 'foo' } }
+
+ it { is_expected.to eq(:instance) }
+ end
+ end
+
+ describe '#finder_for_endpoint_scope' do
+ subject { object.finder_for_endpoint_scope(package_name) }
+
+ let(:package_name) { package.name }
+
+ context 'when called with project scope' do
+ let(:params) { { id: project.id } }
+
+ it 'returns a PackageFinder for project scope' do
+ expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, project: project)
+
+ subject
+ end
+ end
+
+ context 'when called with instance scope' do
+ let(:params) { { package_name: package_name } }
+
+ it 'returns a PackageFinder for namespace scope' do
+ expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, namespace: group)
+
+ subject
+ end
+ end
+ end
+
+ describe '#project_id_or_nil' do
+ subject { object.project_id_or_nil }
+
+ context 'when called with project scope' do
+ let(:params) { { id: project.id } }
+
+ it { is_expected.to eq(project.id) }
+ end
+
+ context 'when called with namespace scope' do
+ context 'when given an unscoped name' do
+ let(:params) { { package_name: 'foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that does not match a group name' do
+ let(:params) { { package_name: '@nonexistent-group/foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that matches a group name' do
+ let(:params) { { package_name: package.name } }
+
+ it { is_expected.to eq(project.id) }
+
+ context 'with another package with the same name, in another project in the namespace' do
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
+
+ it 'returns the project id for the newest matching package within the scope' do
+ expect(subject).to eq(project2.id)
+ end
+ end
+ end
+
+ context 'with npm_allow_packages_in_multiple_projects disabled' do
+ before do
+ stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
+ end
+
+ context 'when given an unscoped name' do
+ let(:params) { { package_name: 'foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that does not match a group name' do
+ let(:params) { { package_name: '@nonexistent-group/foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that matches a group name' do
+ let(:params) { { package_name: package.name } }
+
+ it { is_expected.to eq(project.id) }
+
+ context 'with another package with the same name, in another project in the namespace' do
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
+
+ it 'returns the project id for the newest matching package within the scope' do
+ expect(subject).to eq(project2.id)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb
index 1bfa62b2379..f4008e09ef2 100644
--- a/spec/tooling/danger/stable_branch_spec.rb
+++ b/spec/tooling/danger/stable_branch_spec.rb
@@ -276,23 +276,54 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
end
end
- describe '#non_security_stable_branch?' do
- subject { stable_branch.non_security_stable_branch? }
-
- where(:stable_branch?, :security_mr?, :expected_result) do
- true | true | false
- false | true | false
- true | false | true
- false | false | false
+ describe '#encourage_package_and_qa_execution?' do
+ subject { stable_branch.encourage_package_and_qa_execution? }
+
+ where(:stable_branch?, :security_mr?, :documentation?, :flaky?, :result) do
+ # security merge requests
+ true | true | true | true | false
+ true | true | true | false | false
+ true | true | false | true | false
+ true | true | false | false | false
+ # canonical merge requests with doc and flaky changes only
+ true | false | true | true | false
+ true | false | true | false | false
+ true | false | false | true | false
+ # canonical merge requests with app code
+ true | false | false | false | true
end
with_them do
before do
- allow(fake_helper).to receive(:mr_target_branch).and_return(stable_branch? ? '15-1-stable-ee' : 'main')
- allow(fake_helper).to receive(:security_mr?).and_return(security_mr?)
+ allow(fake_helper)
+ .to receive(:mr_target_branch)
+ .and_return(stable_branch? ? '15-1-stable-ee' : 'main')
+
+ allow(fake_helper)
+ .to receive(:security_mr?)
+ .and_return(security_mr?)
+
+ allow(fake_helper)
+ .to receive(:has_only_documentation_changes?)
+ .and_return(documentation?)
+
+ changes_by_category =
+ if documentation?
+ { docs: ['foo.md'] }
+ else
+ { graphql: ['bar.rb'] }
+ end
+
+ allow(fake_helper)
+ .to receive(:changes_by_category)
+ .and_return(changes_by_category)
+
+ allow(fake_helper)
+ .to receive(:mr_has_labels?)
+ .and_return(flaky?)
end
- it { is_expected.to eq(expected_result) }
+ it { is_expected.to eq(result) }
end
end
end
diff --git a/spec/views/admin/sessions/two_factor.html.haml_spec.rb b/spec/views/admin/sessions/two_factor.html.haml_spec.rb
index c7e0edbcd58..6503c08b84c 100644
--- a/spec/views/admin/sessions/two_factor.html.haml_spec.rb
+++ b/spec/views/admin/sessions/two_factor.html.haml_spec.rb
@@ -29,14 +29,10 @@ RSpec.describe 'admin/sessions/two_factor.html.haml' do
end
end
- context 'user has u2f active' do
- let(:user) { create(:admin, :two_factor_via_u2f) }
+ context 'user has WebAuthn active' do
+ let(:user) { create(:admin, :two_factor_via_webauthn) }
- before do
- stub_feature_flags(webauthn: false)
- end
-
- it 'shows enter u2f form' do
+ it 'shows enter WebAuthn form' do
render
expect(rendered).to have_css('#js-login-2fa-device.btn')
diff --git a/tooling/danger/stable_branch.rb b/tooling/danger/stable_branch.rb
index aaaf3cbea8c..2d1a4ef0845 100644
--- a/tooling/danger/stable_branch.rb
+++ b/tooling/danger/stable_branch.rb
@@ -58,7 +58,7 @@ module Tooling
# rubocop:disable Style/SignalException
def check!
- return unless non_security_stable_branch?
+ return unless valid_stable_branch?
fail FEATURE_ERROR_MESSAGE if has_feature_label?
fail BUG_ERROR_MESSAGE unless bug_fixes_only?
@@ -79,12 +79,18 @@ module Tooling
end
# rubocop:enable Style/SignalException
- def non_security_stable_branch?
- !!stable_target_branch && !helper.security_mr?
+ def encourage_package_and_qa_execution?
+ valid_stable_branch? &&
+ !has_only_documentation_changes? &&
+ !has_flaky_failure_label?
end
private
+ def valid_stable_branch?
+ !!stable_target_branch && !helper.security_mr?
+ end
+
def package_and_test_status
mr_head_pipeline_id = gitlab.mr_json.dig('head_pipeline', 'id')
return unless mr_head_pipeline_id
diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js
deleted file mode 100644
index a33e5e0ade9..00000000000
--- a/vendor/assets/javascripts/u2f.js
+++ /dev/null
@@ -1,750 +0,0 @@
-//Copyright 2014-2015 Google Inc. All rights reserved.
-
-//Use of this source code is governed by a BSD-style
-//license that can be found in the LICENSE file or at
-//https://developers.google.com/open-source/licenses/bsd
-
-/**
- * @fileoverview The U2F api.
- */
-'use strict';
-
-
-/**
- * Namespace for the U2F api.
- * @type {Object}
- */
-var u2f = u2f || {};
-
-/**
- * FIDO U2F Javascript API Version
- * @number
- */
-var js_api_version;
-
-/**
- * The U2F extension id
- * @const {string}
- */
-// The Chrome packaged app extension ID.
-// Uncomment this if you want to deploy a server instance that uses
-// the package Chrome app and does not require installing the U2F Chrome extension.
-u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
-// The U2F Chrome extension ID.
-// Uncomment this if you want to deploy a server instance that uses
-// the U2F Chrome extension to authenticate.
-// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
-
-
-/**
- * Message types for messsages to/from the extension
- * @const
- * @enum {string}
- */
-u2f.MessageTypes = {
- 'U2F_REGISTER_REQUEST': 'u2f_register_request',
- 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
- 'U2F_SIGN_REQUEST': 'u2f_sign_request',
- 'U2F_SIGN_RESPONSE': 'u2f_sign_response',
- 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
- 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
-};
-
-
-/**
- * Response status codes
- * @const
- * @enum {number}
- */
-u2f.ErrorCodes = {
- 'OK': 0,
- 'OTHER_ERROR': 1,
- 'BAD_REQUEST': 2,
- 'CONFIGURATION_UNSUPPORTED': 3,
- 'DEVICE_INELIGIBLE': 4,
- 'TIMEOUT': 5
-};
-
-
-/**
- * A message for registration requests
- * @typedef {{
- * type: u2f.MessageTypes,
- * appId: ?string,
- * timeoutSeconds: ?number,
- * requestId: ?number
- * }}
- */
-u2f.U2fRequest;
-
-
-/**
- * A message for registration responses
- * @typedef {{
- * type: u2f.MessageTypes,
- * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
- * requestId: ?number
- * }}
- */
-u2f.U2fResponse;
-
-
-/**
- * An error object for responses
- * @typedef {{
- * errorCode: u2f.ErrorCodes,
- * errorMessage: ?string
- * }}
- */
-u2f.Error;
-
-/**
- * Data object for a single sign request.
- * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
- */
-u2f.Transport;
-
-
-/**
- * Data object for a single sign request.
- * @typedef {Array<u2f.Transport>}
- */
-u2f.Transports;
-
-/**
- * Data object for a single sign request.
- * @typedef {{
- * version: string,
- * challenge: string,
- * keyHandle: string,
- * appId: string
- * }}
- */
-u2f.SignRequest;
-
-
-/**
- * Data object for a sign response.
- * @typedef {{
- * keyHandle: string,
- * signatureData: string,
- * clientData: string
- * }}
- */
-u2f.SignResponse;
-
-
-/**
- * Data object for a registration request.
- * @typedef {{
- * version: string,
- * challenge: string
- * }}
- */
-u2f.RegisterRequest;
-
-
-/**
- * Data object for a registration response.
- * @typedef {{
- * version: string,
- * keyHandle: string,
- * transports: Transports,
- * appId: string
- * }}
- */
-u2f.RegisterResponse;
-
-
-/**
- * Data object for a registered key.
- * @typedef {{
- * version: string,
- * keyHandle: string,
- * transports: ?Transports,
- * appId: ?string
- * }}
- */
-u2f.RegisteredKey;
-
-
-/**
- * Data object for a get API register response.
- * @typedef {{
- * js_api_version: number
- * }}
- */
-u2f.GetJsApiVersionResponse;
-
-
-//Low level MessagePort API support
-
-/**
- * Sets up a MessagePort to the U2F extension using the
- * available mechanisms.
- * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
- */
-u2f.getMessagePort = function(callback) {
- if (typeof chrome != 'undefined' && chrome.runtime) {
- // The actual message here does not matter, but we need to get a reply
- // for the callback to run. Thus, send an empty signature request
- // in order to get a failure response.
- var msg = {
- type: u2f.MessageTypes.U2F_SIGN_REQUEST,
- signRequests: []
- };
- chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
- if (!chrome.runtime.lastError) {
- // We are on a whitelisted origin and can talk directly
- // with the extension.
- u2f.getChromeRuntimePort_(callback);
- } else {
- // chrome.runtime was available, but we couldn't message
- // the extension directly, use iframe
- u2f.getIframePort_(callback);
- }
- });
- } else if (u2f.isAndroidChrome_()) {
- u2f.getAuthenticatorPort_(callback);
- } else if (u2f.isIosChrome_()) {
- u2f.getIosPort_(callback);
- } else {
- // chrome.runtime was not available at all, which is normal
- // when this origin doesn't have access to any extensions.
- u2f.getIframePort_(callback);
- }
-};
-
-/**
- * Detect chrome running on android based on the browser's useragent.
- * @private
- */
-u2f.isAndroidChrome_ = function() {
- var userAgent = navigator.userAgent;
- return userAgent.indexOf('Chrome') != -1 &&
- userAgent.indexOf('Android') != -1;
-};
-
-/**
- * Detect chrome running on iOS based on the browser's platform.
- * @private
- */
-u2f.isIosChrome_ = function() {
- return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
-};
-
-/**
- * Connects directly to the extension via chrome.runtime.connect.
- * @param {function(u2f.WrappedChromeRuntimePort_)} callback
- * @private
- */
-u2f.getChromeRuntimePort_ = function(callback) {
- var port = chrome.runtime.connect(u2f.EXTENSION_ID,
- {'includeTlsChannelId': true});
- setTimeout(function() {
- callback(new u2f.WrappedChromeRuntimePort_(port));
- }, 0);
-};
-
-/**
- * Return a 'port' abstraction to the Authenticator app.
- * @param {function(u2f.WrappedAuthenticatorPort_)} callback
- * @private
- */
-u2f.getAuthenticatorPort_ = function(callback) {
- setTimeout(function() {
- callback(new u2f.WrappedAuthenticatorPort_());
- }, 0);
-};
-
-/**
- * Return a 'port' abstraction to the iOS client app.
- * @param {function(u2f.WrappedIosPort_)} callback
- * @private
- */
-u2f.getIosPort_ = function(callback) {
- setTimeout(function() {
- callback(new u2f.WrappedIosPort_());
- }, 0);
-};
-
-/**
- * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
- * @param {Port} port
- * @constructor
- * @private
- */
-u2f.WrappedChromeRuntimePort_ = function(port) {
- this.port_ = port;
-};
-
-/**
- * Format and return a sign request compliant with the JS API version supported by the extension.
- * @param {Array<u2f.SignRequest>} signRequests
- * @param {number} timeoutSeconds
- * @param {number} reqId
- * @return {Object}
- */
-u2f.formatSignRequest_ =
- function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
- if (js_api_version === undefined || js_api_version < 1.1) {
- // Adapt request to the 1.0 JS API
- var signRequests = [];
- for (var i = 0; i < registeredKeys.length; i++) {
- signRequests[i] = {
- version: registeredKeys[i].version,
- challenge: challenge,
- keyHandle: registeredKeys[i].keyHandle,
- appId: appId
- };
- }
- return {
- type: u2f.MessageTypes.U2F_SIGN_REQUEST,
- signRequests: signRequests,
- timeoutSeconds: timeoutSeconds,
- requestId: reqId
- };
- }
- // JS 1.1 API
- return {
- type: u2f.MessageTypes.U2F_SIGN_REQUEST,
- appId: appId,
- challenge: challenge,
- registeredKeys: registeredKeys,
- timeoutSeconds: timeoutSeconds,
- requestId: reqId
- };
- };
-
-/**
- * Format and return a register request compliant with the JS API version supported by the extension..
- * @param {Array<u2f.SignRequest>} signRequests
- * @param {Array<u2f.RegisterRequest>} signRequests
- * @param {number} timeoutSeconds
- * @param {number} reqId
- * @return {Object}
- */
-u2f.formatRegisterRequest_ =
- function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
- if (js_api_version === undefined || js_api_version < 1.1) {
- // Adapt request to the 1.0 JS API
- for (var i = 0; i < registerRequests.length; i++) {
- registerRequests[i].appId = appId;
- }
- var signRequests = [];
- for (var i = 0; i < registeredKeys.length; i++) {
- signRequests[i] = {
- version: registeredKeys[i].version,
- challenge: registerRequests[0],
- keyHandle: registeredKeys[i].keyHandle,
- appId: appId
- };
- }
- return {
- type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
- signRequests: signRequests,
- registerRequests: registerRequests,
- timeoutSeconds: timeoutSeconds,
- requestId: reqId
- };
- }
- // JS 1.1 API
- return {
- type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
- appId: appId,
- registerRequests: registerRequests,
- registeredKeys: registeredKeys,
- timeoutSeconds: timeoutSeconds,
- requestId: reqId
- };
- };
-
-
-/**
- * Posts a message on the underlying channel.
- * @param {Object} message
- */
-u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
- this.port_.postMessage(message);
-};
-
-
-/**
- * Emulates the HTML 5 addEventListener interface. Works only for the
- * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
- * @param {string} eventName
- * @param {function({data: Object})} handler
- */
-u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
- function(eventName, handler) {
- var name = eventName.toLowerCase();
- if (name == 'message' || name == 'onmessage') {
- this.port_.onMessage.addListener(function(message) {
- // Emulate a minimal MessageEvent object
- handler({'data': message});
- });
- } else {
- console.error('WrappedChromeRuntimePort only supports onMessage');
- }
- };
-
-/**
- * Wrap the Authenticator app with a MessagePort interface.
- * @constructor
- * @private
- */
-u2f.WrappedAuthenticatorPort_ = function() {
- this.requestId_ = -1;
- this.requestObject_ = null;
-}
-
-/**
- * Launch the Authenticator intent.
- * @param {Object} message
- */
-u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
- var intentUrl =
- u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
- ';S.request=' + encodeURIComponent(JSON.stringify(message)) +
- ';end';
- document.location = intentUrl;
-};
-
-/**
- * Tells what type of port this is.
- * @return {String} port type
- */
-u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
- return "WrappedAuthenticatorPort_";
-};
-
-
-/**
- * Emulates the HTML 5 addEventListener interface.
- * @param {string} eventName
- * @param {function({data: Object})} handler
- */
-u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
- var name = eventName.toLowerCase();
- if (name == 'message') {
- var self = this;
- /* Register a callback to that executes when
- * chrome injects the response. */
- window.addEventListener(
- 'message', self.onRequestUpdate_.bind(self, handler), false);
- } else {
- console.error('WrappedAuthenticatorPort only supports message');
- }
-};
-
-/**
- * Callback invoked when a response is received from the Authenticator.
- * @param function({data: Object}) callback
- * @param {Object} message message Object
- */
-u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
- function(callback, message) {
- var messageObject = JSON.parse(message.data);
- var intentUrl = messageObject['intentURL'];
-
- var errorCode = messageObject['errorCode'];
- var responseObject = null;
- if (messageObject.hasOwnProperty('data')) {
- responseObject = /** @type {Object} */ (
- JSON.parse(messageObject['data']));
- }
-
- callback({'data': responseObject});
- };
-
-/**
- * Base URL for intents to Authenticator.
- * @const
- * @private
- */
-u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
- 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
-
-/**
- * Wrap the iOS client app with a MessagePort interface.
- * @constructor
- * @private
- */
-u2f.WrappedIosPort_ = function() {};
-
-/**
- * Launch the iOS client app request
- * @param {Object} message
- */
-u2f.WrappedIosPort_.prototype.postMessage = function(message) {
- var str = JSON.stringify(message);
- var url = "u2f://auth?" + encodeURI(str);
- location.replace(url);
-};
-
-/**
- * Tells what type of port this is.
- * @return {String} port type
- */
-u2f.WrappedIosPort_.prototype.getPortType = function() {
- return "WrappedIosPort_";
-};
-
-/**
- * Emulates the HTML 5 addEventListener interface.
- * @param {string} eventName
- * @param {function({data: Object})} handler
- */
-u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
- var name = eventName.toLowerCase();
- if (name !== 'message') {
- console.error('WrappedIosPort only supports message');
- }
-};
-
-/**
- * Sets up an embedded trampoline iframe, sourced from the extension.
- * @param {function(MessagePort)} callback
- * @private
- */
-u2f.getIframePort_ = function(callback) {
- // Create the iframe
- var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
- var iframe = document.createElement('iframe');
- iframe.src = iframeOrigin + '/u2f-comms.html';
- iframe.setAttribute('style', 'display:none');
- document.body.appendChild(iframe);
-
- var channel = new MessageChannel();
- var ready = function(message) {
- if (message.data == 'ready') {
- channel.port1.removeEventListener('message', ready);
- callback(channel.port1);
- } else {
- console.error('First event on iframe port was not "ready"');
- }
- };
- channel.port1.addEventListener('message', ready);
- channel.port1.start();
-
- iframe.addEventListener('load', function() {
- // Deliver the port to the iframe and initialize
- iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
- });
-};
-
-
-//High-level JS API
-
-/**
- * Default extension response timeout in seconds.
- * @const
- */
-u2f.EXTENSION_TIMEOUT_SEC = 30;
-
-/**
- * A singleton instance for a MessagePort to the extension.
- * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
- * @private
- */
-u2f.port_ = null;
-
-/**
- * Callbacks waiting for a port
- * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
- * @private
- */
-u2f.waitingForPort_ = [];
-
-/**
- * A counter for requestIds.
- * @type {number}
- * @private
- */
-u2f.reqCounter_ = 0;
-
-/**
- * A map from requestIds to client callbacks
- * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
- * |function((u2f.Error|u2f.SignResponse)))>}
- * @private
- */
-u2f.callbackMap_ = {};
-
-/**
- * Creates or retrieves the MessagePort singleton to use.
- * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
- * @private
- */
-u2f.getPortSingleton_ = function(callback) {
- if (u2f.port_) {
- callback(u2f.port_);
- } else {
- if (u2f.waitingForPort_.length == 0) {
- u2f.getMessagePort(function(port) {
- u2f.port_ = port;
- u2f.port_.addEventListener('message',
- /** @type {function(Event)} */ (u2f.responseHandler_));
-
- // Careful, here be async callbacks. Maybe.
- while (u2f.waitingForPort_.length)
- u2f.waitingForPort_.shift()(u2f.port_);
- });
- }
- u2f.waitingForPort_.push(callback);
- }
-};
-
-/**
- * Handles response messages from the extension.
- * @param {MessageEvent.<u2f.Response>} message
- * @private
- */
-u2f.responseHandler_ = function(message) {
- var response = message.data;
- var reqId = response['requestId'];
- if (!reqId || !u2f.callbackMap_[reqId]) {
- console.error('Unknown or missing requestId in response.');
- return;
- }
- var cb = u2f.callbackMap_[reqId];
- delete u2f.callbackMap_[reqId];
- cb(response['responseData']);
-};
-
-/**
- * Dispatches an array of sign requests to available U2F tokens.
- * If the JS API version supported by the extension is unknown, it first sends a
- * message to the extension to find out the supported API version and then it sends
- * the sign request.
- * @param {string=} appId
- * @param {string=} challenge
- * @param {Array<u2f.RegisteredKey>} registeredKeys
- * @param {function((u2f.Error|u2f.SignResponse))} callback
- * @param {number=} opt_timeoutSeconds
- */
-u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
- if (js_api_version === undefined) {
- // Send a message to get the extension to JS API version, then send the actual sign request.
- u2f.getApiVersion(
- function (response) {
- js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
- console.log("Extension JS API Version: ", js_api_version);
- u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
- });
- } else {
- // We know the JS API version. Send the actual sign request in the supported API version.
- u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
- }
-};
-
-/**
- * Dispatches an array of sign requests to available U2F tokens.
- * @param {string=} appId
- * @param {string=} challenge
- * @param {Array<u2f.RegisteredKey>} registeredKeys
- * @param {function((u2f.Error|u2f.SignResponse))} callback
- * @param {number=} opt_timeoutSeconds
- */
-u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
- u2f.getPortSingleton_(function(port) {
- var reqId = ++u2f.reqCounter_;
- u2f.callbackMap_[reqId] = callback;
- var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
- opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
- var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
- port.postMessage(req);
- });
-};
-
-/**
- * Dispatches register requests to available U2F tokens. An array of sign
- * requests identifies already registered tokens.
- * If the JS API version supported by the extension is unknown, it first sends a
- * message to the extension to find out the supported API version and then it sends
- * the register request.
- * @param {string=} appId
- * @param {Array<u2f.RegisterRequest>} registerRequests
- * @param {Array<u2f.RegisteredKey>} registeredKeys
- * @param {function((u2f.Error|u2f.RegisterResponse))} callback
- * @param {number=} opt_timeoutSeconds
- */
-u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
- if (js_api_version === undefined) {
- // Send a message to get the extension to JS API version, then send the actual register request.
- u2f.getApiVersion(
- function (response) {
- js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
- console.log("Extension JS API Version: ", js_api_version);
- u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
- callback, opt_timeoutSeconds);
- });
- } else {
- // We know the JS API version. Send the actual register request in the supported API version.
- u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
- callback, opt_timeoutSeconds);
- }
-};
-
-/**
- * Dispatches register requests to available U2F tokens. An array of sign
- * requests identifies already registered tokens.
- * @param {string=} appId
- * @param {Array<u2f.RegisterRequest>} registerRequests
- * @param {Array<u2f.RegisteredKey>} registeredKeys
- * @param {function((u2f.Error|u2f.RegisterResponse))} callback
- * @param {number=} opt_timeoutSeconds
- */
-u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
- u2f.getPortSingleton_(function(port) {
- var reqId = ++u2f.reqCounter_;
- u2f.callbackMap_[reqId] = callback;
- var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
- opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
- var req = u2f.formatRegisterRequest_(
- appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
- port.postMessage(req);
- });
-};
-
-
-/**
- * Dispatches a message to the extension to find out the supported
- * JS API version.
- * If the user is on a mobile phone and is thus using Google Authenticator instead
- * of the Chrome extension, don't send the request and simply return 0.
- * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
- * @param {number=} opt_timeoutSeconds
- */
-u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
- u2f.getPortSingleton_(function(port) {
- // If we are using Android Google Authenticator or iOS client app,
- // do not fire an intent to ask which JS API version to use.
- if (port.getPortType) {
- var apiVersion;
- switch (port.getPortType()) {
- case 'WrappedIosPort_':
- case 'WrappedAuthenticatorPort_':
- apiVersion = 1.1;
- break;
-
- default:
- apiVersion = 0;
- break;
- }
- callback({ 'js_api_version': apiVersion });
- return;
- }
- var reqId = ++u2f.reqCounter_;
- u2f.callbackMap_[reqId] = callback;
- var req = {
- type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
- timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
- opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
- requestId: reqId
- };
- port.postMessage(req);
- });
-};
-
-window.u2f || (window.u2f = u2f);