From a8653790086d284cecffdc35892bb422cd6c9a7b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 7 Sep 2020 12:08:27 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/docs.gitlab-ci.yml | 1 + .haml-lint_todo.yml | 4 +- Gemfile | 2 + Gemfile.lock | 25 ++ .../javascripts/authentication/mount_2fa.js | 19 +- .../authentication/u2f/authenticate.js | 3 +- .../authentication/u2f/register.js | 26 +- .../authentication/webauthn/authenticate.js | 69 ++++++ .../authentication/webauthn/error.js | 28 +++ .../authentication/webauthn/flow.js | 24 ++ .../authentication/webauthn/index.js | 13 + .../authentication/webauthn/register.js | 78 ++++++ .../authentication/webauthn/util.js | 120 +++++++++ .../diffs/components/diff_file_header.vue | 39 ++- .../details/components/dependency_row.vue | 2 +- .../components/delete_milestone_modal.vue | 7 +- .../pipelines/pipeline_details_bundle.js | 22 +- .../stores/get_state_key.js | 2 +- app/assets/stylesheets/framework/tables.scss | 6 +- .../stylesheets/page_bundles/todos.scss | 16 +- app/assets/stylesheets/pages/profile.scss | 3 +- app/assets/stylesheets/utilities.scss | 23 +- .../authenticates_2fa_for_admin_mode.rb | 50 +++- .../concerns/authenticates_with_two_factor.rb | 64 ++++- .../profiles/two_factor_auths_controller.rb | 90 ++++++- .../webauthn_registrations_controller.rb | 10 + app/controllers/profiles_controller.rb | 3 + .../merge_requests/application_controller.rb | 1 + app/controllers/projects_controller.rb | 2 +- app/controllers/sessions_controller.rb | 7 +- app/graphql/types/issuable_severity_enum.rb | 12 + app/graphql/types/issue_type.rb | 3 + app/helpers/diff_helper.rb | 37 ++- app/helpers/submodule_helper.rb | 52 ++-- app/models/ci/build_trace_chunk.rb | 16 +- app/models/ci/pipeline.rb | 8 +- app/models/concerns/enums/ci/pipeline.rb | 35 ++- app/models/concerns/issuable.rb | 10 + app/models/issuable_severity.rb | 2 + app/models/issue.rb | 1 + app/models/lfs_objects_project.rb | 1 + app/models/members_preloader.rb | 1 + app/models/merge_request.rb | 9 +- app/models/project.rb | 13 +- app/models/user.rb | 26 +- app/serializers/diff_file_base_entity.rb | 24 +- app/serializers/merge_request_basic_entity.rb | 1 + .../merge_request_reviewer_entity.rb | 9 + .../merge_request_sidebar_extras_entity.rb | 4 + app/services/issuable_base_service.rb | 10 +- app/services/merge_requests/base_service.rb | 22 ++ app/services/merge_requests/merge_service.rb | 5 +- app/services/webauthn/authenticate_service.rb | 61 +++++ app/services/webauthn/register_service.rb | 34 +++ .../admin/sessions/_two_factor_otp.html.haml | 2 +- app/views/admin/sessions/two_factor.html.haml | 4 +- .../_authenticate.html.haml | 2 +- app/views/authentication/_register.html.haml | 37 +++ .../devise/sessions/two_factor.html.haml | 7 +- .../profiles/two_factor_auths/show.html.haml | 43 +++- app/views/projects/diffs/_file.html.haml | 4 + app/views/u2f/_register.html.haml | 40 --- app/workers/all_queues.yml | 2 +- .../ci/build_trace_chunk_flush_worker.rb | 4 +- ...9-add-created-at-index-on-audit-events.yml | 5 + ...20916-new-comment-not-drop-merge-train.yml | 5 + .../unreleased/22506-webauthn-step-1.yml | 5 + ...ee-app-views-groups-push_rules-edit-ht.yml | 5 + ...cident-severity-to-issue-graphql-query.yml | 5 + .../244848-fix-lfs-cleanup-data-loss-bug.yml | 5 + .../compare-link-git-submodule-update.yml | 5 + .../unreleased/feature-issue_241962.yml | 5 + .../development/merge_request_reviewers.yml | 7 + config/initializers/webauthn.rb | 35 +++ config/routes/profile.rb | 2 + ...31_add_created_at_index_to_audit_events.rb | 22 ++ db/schema_migrations/20200903064431 | 1 + db/structure.sql | 4 +- .../geo/disaster_recovery/index.md | 4 +- doc/administration/pages/index.md | 3 +- doc/administration/postgresql/external.md | 3 +- .../graphql/reference/gitlab_schema.graphql | 70 ++++++ doc/api/graphql/reference/gitlab_schema.json | 122 +++++++++ doc/api/graphql/reference/index.md | 2 + doc/api/issues.md | 175 +------------ doc/development/telemetry/usage_ping.md | 23 ++ .../admin_area/analytics/dev_ops_report.md | 4 +- ..._approval_rules_and_enabled_jobs_v13_4.png | Bin 0 -> 99883 bytes ...security_approval_rules_and_jobs_v13_4.png | Bin 0 -> 82526 bytes .../img/vulnerability-check_v13_0.png | Bin 30789 -> 0 bytes .../img/vulnerability-check_v13_4.png | Bin 0 -> 75105 bytes doc/user/application_security/index.md | 54 ++-- .../img/license-check_v13_4.png | Bin 0 -> 74407 bytes .../compliance/license_compliance/index.md | 14 +- haml_lint/linter/documentation_links.rb | 3 +- lib/api/helpers.rb | 14 ++ lib/api/users.rb | 1 + .../cleanup/orphan_lfs_file_references.rb | 30 +-- .../representation/submodule_tree_entry.rb | 4 +- lib/gitlab/submodule_links.rb | 18 +- lib/gitlab/utils/markdown.rb | 2 + locale/gitlab.pot | 64 ++++- .../admin/sessions_controller_spec.rb | 31 ++- .../webauthn_registrations_controller_spec.rb | 20 ++ spec/controllers/sessions_controller_spec.rb | 4 + spec/factories/users.rb | 8 + spec/factories/webauthn_registrations.rb | 11 + spec/features/u2f_spec.rb | 115 +-------- spec/features/webauthn_spec.rb | 234 ++++++++++++++++++ .../schemas/entities/merge_request_basic.json | 7 + .../merge_request_sidebar_extras.json | 4 + .../authentication/u2f/authenticate_spec.js | 6 +- .../authentication/u2f/register_spec.js | 18 +- .../webauthn/authenticate_spec.js | 132 ++++++++++ .../authentication/webauthn/error_spec.js | 50 ++++ .../webauthn/mock_webauthn_device.js | 35 +++ .../authentication/webauthn/register_spec.js | 131 ++++++++++ spec/frontend/authentication/webauthn/util.js | 19 ++ spec/frontend/fixtures/u2f.rb | 4 + spec/frontend/fixtures/webauthn.rb | 47 ++++ .../__snapshots__/dependency_row_spec.js.snap | 2 +- .../stores/get_state_key_spec.js | 2 + .../project_pipeline_resolver_spec.rb | 8 +- .../types/issuable_severity_enum_spec.rb | 13 + spec/graphql/types/issue_type_spec.rb | 2 +- .../linter/documentation_links_spec.rb | 7 + spec/helpers/submodule_helper_spec.rb | 174 ++++++++++--- spec/lib/api/helpers_spec.rb | 56 +++++ .../filter/table_of_contents_filter_spec.rb | 5 + .../orphan_lfs_file_references_spec.rb | 51 +++- spec/lib/gitlab/import_export/all_models.yml | 3 + spec/lib/gitlab/submodule_links_spec.rb | 58 ++++- spec/lib/gitlab/utils/markdown_spec.rb | 16 ++ spec/models/ci/pipeline_spec.rb | 62 ++++- spec/models/ci/ref_spec.rb | 8 +- spec/models/concerns/issuable_spec.rb | 37 +++ spec/models/deployment_spec.rb | 8 +- spec/models/issue_spec.rb | 10 + spec/models/merge_request_spec.rb | 99 +++++++- spec/models/project_spec.rb | 3 +- spec/models/user_spec.rb | 94 ++++--- spec/requests/api/ci/pipelines_spec.rb | 13 +- spec/requests/api/jobs_spec.rb | 8 +- .../serializers/diff_file_base_entity_spec.rb | 53 +++- .../merge_request_basic_entity_spec.rb | 27 +- ...erge_request_sidebar_extras_entity_spec.rb | 57 +++++ .../merge_requests/create_service_spec.rb | 4 + .../merge_requests/merge_service_spec.rb | 37 +++ .../merge_requests/update_service_spec.rb | 23 ++ spec/services/notification_service_spec.rb | 9 +- .../webauthn/authenticate_service_spec.rb | 48 ++++ .../webauthn/register_service_spec.rb | 36 +++ spec/support/helpers/fake_u2f_device.rb | 3 +- spec/support/helpers/fake_webauthn_device.rb | 74 ++++++ .../helpers/features/two_factor_helpers.rb | 74 ++++++ spec/support/helpers/login_helpers.rb | 5 + .../features/2fa_shared_examples.rb | 108 ++++++++ .../services/merge_request_shared_examples.rb | 62 +++++ .../sessions/two_factor.html.haml_spec.rb | 4 + .../ci/build_trace_chunk_flush_worker_spec.rb | 31 +++ 160 files changed, 3558 insertions(+), 733 deletions(-) create mode 100644 app/assets/javascripts/authentication/webauthn/authenticate.js create mode 100644 app/assets/javascripts/authentication/webauthn/error.js create mode 100644 app/assets/javascripts/authentication/webauthn/flow.js create mode 100644 app/assets/javascripts/authentication/webauthn/index.js create mode 100644 app/assets/javascripts/authentication/webauthn/register.js create mode 100644 app/assets/javascripts/authentication/webauthn/util.js create mode 100644 app/controllers/profiles/webauthn_registrations_controller.rb create mode 100644 app/graphql/types/issuable_severity_enum.rb create mode 100644 app/serializers/merge_request_reviewer_entity.rb create mode 100644 app/services/webauthn/authenticate_service.rb create mode 100644 app/services/webauthn/register_service.rb rename app/views/{u2f => authentication}/_authenticate.html.haml (93%) create mode 100644 app/views/authentication/_register.html.haml delete mode 100644 app/views/u2f/_register.html.haml create mode 100644 changelogs/unreleased/1449-add-created-at-index-on-audit-events.yml create mode 100644 changelogs/unreleased/220916-new-comment-not-drop-merge-train.yml create mode 100644 changelogs/unreleased/22506-webauthn-step-1.yml create mode 100644 changelogs/unreleased/233652-replace-bootstrap-alerts-in-ee-app-views-groups-push_rules-edit-ht.yml create mode 100644 changelogs/unreleased/238568-add-incident-severity-to-issue-graphql-query.yml create mode 100644 changelogs/unreleased/244848-fix-lfs-cleanup-data-loss-bug.yml create mode 100644 changelogs/unreleased/compare-link-git-submodule-update.yml create mode 100644 changelogs/unreleased/feature-issue_241962.yml create mode 100644 config/feature_flags/development/merge_request_reviewers.yml create mode 100644 config/initializers/webauthn.rb create mode 100644 db/post_migrate/20200903064431_add_created_at_index_to_audit_events.rb create mode 100644 db/schema_migrations/20200903064431 create mode 100644 doc/user/application_security/img/unconfigured_security_approval_rules_and_enabled_jobs_v13_4.png create mode 100644 doc/user/application_security/img/unconfigured_security_approval_rules_and_jobs_v13_4.png delete mode 100644 doc/user/application_security/img/vulnerability-check_v13_0.png create mode 100644 doc/user/application_security/img/vulnerability-check_v13_4.png create mode 100644 doc/user/compliance/license_compliance/img/license-check_v13_4.png create mode 100644 spec/controllers/profiles/webauthn_registrations_controller_spec.rb create mode 100644 spec/factories/webauthn_registrations.rb create mode 100644 spec/features/webauthn_spec.rb create mode 100644 spec/frontend/authentication/webauthn/authenticate_spec.js create mode 100644 spec/frontend/authentication/webauthn/error_spec.js create mode 100644 spec/frontend/authentication/webauthn/mock_webauthn_device.js create mode 100644 spec/frontend/authentication/webauthn/register_spec.js create mode 100644 spec/frontend/authentication/webauthn/util.js create mode 100644 spec/frontend/fixtures/webauthn.rb create mode 100644 spec/graphql/types/issuable_severity_enum_spec.rb create mode 100644 spec/serializers/merge_request_sidebar_extras_entity_spec.rb create mode 100644 spec/services/webauthn/authenticate_service_spec.rb create mode 100644 spec/services/webauthn/register_service_spec.rb create mode 100644 spec/support/helpers/fake_webauthn_device.rb create mode 100644 spec/support/helpers/features/two_factor_helpers.rb create mode 100644 spec/support/shared_examples/features/2fa_shared_examples.rb create mode 100644 spec/support/shared_examples/services/merge_request_shared_examples.rb create mode 100644 spec/workers/ci/build_trace_chunk_flush_worker_spec.rb diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index b871ba33974..e35f491db79 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -45,6 +45,7 @@ docs lint: image: "registry.gitlab.com/gitlab-org/gitlab-docs/lint:vale-2.3.3-markdownlint-0.23.2" stage: test needs: [] + allow_failure: true script: - scripts/lint-doc.sh # Prepare docs for build diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 4163c7bacd1..83aba188d2b 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -63,6 +63,8 @@ linters: - "app/views/admin/users/new.html.haml" - "app/views/admin/users/projects.html.haml" - "app/views/admin/users/show.html.haml" + - 'app/views/authentication/_authenticate.html.haml' + - 'app/views/authentication/_register.html.haml' - "app/views/clusters/clusters/_cluster.html.haml" - "app/views/clusters/clusters/new.html.haml" - "app/views/dashboard/milestones/index.html.haml" @@ -311,8 +313,6 @@ linters: - "app/views/shared/web_hooks/_form.html.haml" - "app/views/shared/web_hooks/_hook.html.haml" - "app/views/shared/wikis/_pages_wiki_page.html.haml" - - "app/views/u2f/_authenticate.html.haml" - - "app/views/u2f/_register.html.haml" - "app/views/users/_deletion_guidance.html.haml" - "ee/app/views/admin/_namespace_plan_info.html.haml" - "ee/app/views/admin/application_settings/_templates.html.haml" diff --git a/Gemfile b/Gemfile index 89cb29d3034..337c8063f39 100644 --- a/Gemfile +++ b/Gemfile @@ -512,3 +512,5 @@ gem 'json_schemer', '~> 0.2.12' gem 'oj', '~> 3.10.6' gem 'multi_json', '~> 1.14.1' gem 'yajl-ruby', '~> 1.4.1', require: 'yajl' + +gem 'webauthn', '~> 2.3' diff --git a/Gemfile.lock b/Gemfile.lock index b6daa9abaa7..fcd5be4afe6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM public_suffix (>= 2.0.2, < 5.0) aes_key_wrap (1.0.1) akismet (3.0.0) + android_key_attestation (0.3.0) apollo_upload_server (2.0.2) graphql (>= 1.8) rails (>= 4.2) @@ -93,6 +94,7 @@ GEM encryptor (~> 3.0.0) attr_required (1.0.1) awesome_print (1.8.0) + awrence (1.1.1) aws-eventstream (1.1.0) aws-partitions (1.345.0) aws-sdk-cloudformation (1.41.0) @@ -167,6 +169,7 @@ GEM activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) + cbor (0.5.9.6) character_set (1.4.0) charlock_holmes (0.7.6) childprocess (3.0.0) @@ -189,6 +192,9 @@ GEM contracts (0.11.0) cork (0.3.0) colored2 (~> 3.1) + cose (1.0.0) + cbor (~> 0.5.9) + openssl-signature_algorithm (~> 0.4.0) countries (3.0.0) i18n_data (~> 0.8.0) sixarm_ruby_unaccent (~> 1.1) @@ -802,6 +808,8 @@ GEM validate_email validate_url webfinger (>= 1.0.1) + openssl (2.2.0) + openssl-signature_algorithm (0.4.0) opentracing (0.5.0) optimist (3.0.1) org-ruby (0.9.12) @@ -1026,6 +1034,8 @@ GEM rubyzip (2.0.0) rugged (0.28.4.1) safe_yaml (1.0.4) + safety_net_attestation (0.4.0) + jwt (~> 2.0) sanitize (5.2.1) crass (~> 1.0.2) nokogiri (>= 1.8.0) @@ -1050,6 +1060,7 @@ GEM scss_lint (0.56.0) rake (>= 0.9, < 13) sass (~> 3.5.3) + securecompare (1.0.0) seed-fu (2.3.7) activerecord (>= 3.1) activesupport (>= 3.1) @@ -1135,6 +1146,9 @@ GEM parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) + tpm-key_attestation (0.9.0) + bindata (~> 2.4) + openssl-signature_algorithm (~> 0.4.0) truncato (0.7.11) htmlentities (~> 4.3.1) nokogiri (>= 1.7.0, <= 2.0) @@ -1186,6 +1200,16 @@ GEM vmstat (2.3.0) warden (1.2.8) rack (>= 2.0.6) + webauthn (2.3.0) + android_key_attestation (~> 0.3.0) + awrence (~> 1.1) + bindata (~> 2.4) + cbor (~> 0.5.9) + cose (~> 1.0) + openssl (~> 2.0) + safety_net_attestation (~> 0.4.0) + securecompare (~> 1.0) + tpm-key_attestation (~> 0.9.0) webfinger (1.1.0) activesupport httpclient (>= 2.4) @@ -1472,6 +1496,7 @@ DEPENDENCIES validates_hostname (~> 1.0.10) version_sorter (~> 2.2.4) vmstat (~> 2.3.0) + webauthn (~> 2.3) webmock (~> 3.5.1) webpack-rails (~> 0.9.10) wikicloth (= 0.8.1) diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js index 9917151ac81..dd5a42fa5fc 100644 --- a/app/assets/javascripts/authentication/mount_2fa.js +++ b/app/assets/javascripts/authentication/mount_2fa.js @@ -1,14 +1,23 @@ import $ from 'jquery'; import initU2F from './u2f'; +import initWebauthn from './webauthn'; import U2FRegister from './u2f/register'; +import WebAuthnRegister from './webauthn/register'; export const mount2faAuthentication = () => { - // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692) - initU2F(); + if (gon.webauthn) { + initWebauthn(); + } else { + initU2F(); + } }; export const mount2faRegistration = () => { - // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692) - const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f); - u2fRegister.start(); + if (gon.webauthn) { + const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn); + webauthnRegister.start(); + } else { + const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f); + u2fRegister.start(); + } }; diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js index 201cd5c2e61..f9b5ca3e5b4 100644 --- a/app/assets/javascripts/authentication/u2f/authenticate.js +++ b/app/assets/javascripts/authentication/u2f/authenticate.js @@ -40,7 +40,6 @@ export default class U2FAuthenticate { this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge')); this.templates = { - setup: '#js-authenticate-token-2fa-setup', inProgress: '#js-authenticate-token-2fa-in-progress', error: '#js-authenticate-token-2fa-error', authenticated: '#js-authenticate-token-2fa-authenticated', @@ -86,7 +85,7 @@ export default class U2FAuthenticate { renderError(error) { this.renderTemplate('error', { error_message: error.message(), - error_code: error.errorCode, + error_name: error.errorCode, }); return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress); } diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js index 52c0ce1fc04..9773a9185f8 100644 --- a/app/assets/javascripts/authentication/u2f/register.js +++ b/app/assets/javascripts/authentication/u2f/register.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { template as lodashTemplate } from 'lodash'; +import { __ } from '~/locale'; import importU2FLibrary from './util'; import U2FError from './error'; @@ -24,11 +25,10 @@ export default class U2FRegister { this.signRequests = u2fParams.sign_requests; this.templates = { - notSupported: '#js-register-u2f-not-supported', - setup: '#js-register-u2f-setup', - inProgress: '#js-register-u2f-in-progress', - error: '#js-register-u2f-error', - registered: '#js-register-u2f-registered', + message: '#js-register-2fa-message', + setup: '#js-register-token-2fa-setup', + error: '#js-register-token-2fa-error', + registered: '#js-register-token-2fa-registered', }; } @@ -65,18 +65,22 @@ export default class U2FRegister { renderSetup() { this.renderTemplate('setup'); - return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); + return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress); } renderInProgress() { - this.renderTemplate('inProgress'); + 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_code: error.errorCode, + error_name: error.errorCode, }); return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup); } @@ -89,6 +93,10 @@ export default class U2FRegister { } renderNotSupported() { - return this.renderTemplate('notSupported'); + 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/webauthn/authenticate.js b/app/assets/javascripts/authentication/webauthn/authenticate.js new file mode 100644 index 00000000000..42c4c2b63bd --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/authenticate.js @@ -0,0 +1,69 @@ +import WebAuthnError from './error'; +import WebAuthnFlow from './flow'; +import { supported, convertGetParams, convertGetResponse } from './util'; + +// Authenticate WebAuthn 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 WebAuthnAuthenticate { + constructor(container, form, webauthnParams, fallbackButton, fallbackUI) { + this.container = container; + this.webauthnParams = convertGetParams(JSON.parse(webauthnParams.options)); + this.renderInProgress = this.renderInProgress.bind(this); + + this.form = form; + this.fallbackButton = fallbackButton; + this.fallbackUI = fallbackUI; + if (this.fallbackButton) { + this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); + } + + this.flow = new WebAuthnFlow(container, { + inProgress: '#js-authenticate-token-2fa-in-progress', + error: '#js-authenticate-token-2fa-error', + authenticated: '#js-authenticate-token-2fa-authenticated', + }); + + this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress); + } + + start() { + if (!supported()) { + this.switchToFallbackUI(); + } else { + this.renderInProgress(); + } + } + + authenticate() { + navigator.credentials + .get({ publicKey: this.webauthnParams }) + .then(resp => { + const convertedResponse = convertGetResponse(resp); + this.renderAuthenticated(JSON.stringify(convertedResponse)); + }) + .catch(err => { + this.flow.renderError(new WebAuthnError(err, 'authenticate')); + }); + } + + renderInProgress() { + this.flow.renderTemplate('inProgress'); + this.authenticate(); + } + + renderAuthenticated(deviceResponse) { + this.flow.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/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js new file mode 100644 index 00000000000..a1a3f861c25 --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/error.js @@ -0,0 +1,28 @@ +import { __ } from '~/locale'; +import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util'; + +export default class WebAuthnError { + constructor(error, flowType) { + this.error = error; + this.errorName = error.name || 'UnknownError'; + this.message = this.message.bind(this); + this.httpsDisabled = !isHTTPS(); + this.flowType = flowType; + } + + message() { + if (this.errorName === 'NotSupportedError') { + return __('Your device is not compatible with GitLab. Please try another device'); + } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) { + return __('This device has not been registered with us.'); + } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) { + return __('This device has already been registered with us.'); + } else if (this.errorName === 'SecurityError' && this.httpsDisabled) { + return __( + 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.', + ); + } + + return __('There was a problem communicating with your device.'); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/flow.js b/app/assets/javascripts/authentication/webauthn/flow.js new file mode 100644 index 00000000000..10a1debc876 --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/flow.js @@ -0,0 +1,24 @@ +import { template } from 'lodash'; + +/** + * Generic abstraction for WebAuthnFlows, especially for register / authenticate + */ +export default class WebAuthnFlow { + constructor(container, templates) { + this.container = container; + this.templates = templates; + } + + renderTemplate(name, params) { + const templateString = document.querySelector(this.templates[name]).innerHTML; + const compiledTemplate = template(templateString); + this.container.html(compiledTemplate(params)); + } + + renderError(error) { + this.renderTemplate('error', { + error_message: error.message(), + error_name: error.errorName, + }); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/index.js b/app/assets/javascripts/authentication/webauthn/index.js new file mode 100644 index 00000000000..bbf694c7698 --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/index.js @@ -0,0 +1,13 @@ +import $ from 'jquery'; +import WebAuthnAuthenticate from './authenticate'; + +export default () => { + const webauthnAuthenticate = new WebAuthnAuthenticate( + $('#js-authenticate-token-2fa'), + '#js-login-token-2fa-form', + gon.webauthn, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + webauthnAuthenticate.start(); +}; diff --git a/app/assets/javascripts/authentication/webauthn/register.js b/app/assets/javascripts/authentication/webauthn/register.js new file mode 100644 index 00000000000..06e4ffd6f3e --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/register.js @@ -0,0 +1,78 @@ +import { __ } from '~/locale'; +import WebAuthnError from './error'; +import WebAuthnFlow from './flow'; +import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util'; + +// Register WebAuthn 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 WebAuthnRegister { + constructor(container, webauthnParams) { + this.container = container; + this.renderInProgress = this.renderInProgress.bind(this); + this.webauthnOptions = convertCreateParams(webauthnParams.options); + + this.flow = new WebAuthnFlow(container, { + message: '#js-register-2fa-message', + setup: '#js-register-token-2fa-setup', + error: '#js-register-token-2fa-error', + registered: '#js-register-token-2fa-registered', + }); + + this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress); + } + + start() { + if (!supported()) { + // we show a special error message when the user visits the site + // using a non-ssl connection as this makes WebAuthn unavailable in + // any case, regardless of the used browser + this.renderNotSupported(!isHTTPS()); + } else { + this.renderSetup(); + } + } + + register() { + navigator.credentials + .create({ + publicKey: this.webauthnOptions, + }) + .then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred)))) + .catch(err => this.flow.renderError(new WebAuthnError(err, 'register'))); + } + + renderSetup() { + this.flow.renderTemplate('setup'); + this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress); + } + + renderInProgress() { + this.flow.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(); + } + + renderRegistered(deviceResponse) { + this.flow.renderTemplate('registered'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. + this.container.find('#js-device-response').val(deviceResponse); + } + + renderNotSupported(noHttps) { + const message = noHttps + ? __( + 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.', + ) + : __( + "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).", + ); + + this.flow.renderTemplate('message', { message }); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/util.js b/app/assets/javascripts/authentication/webauthn/util.js new file mode 100644 index 00000000000..5f06c000afe --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/util.js @@ -0,0 +1,120 @@ +export function supported() { + return Boolean( + navigator.credentials && + navigator.credentials.create && + navigator.credentials.get && + window.PublicKeyCredential, + ); +} + +export function isHTTPS() { + return window.location.protocol.startsWith('https'); +} + +export const FLOW_AUTHENTICATE = 'authenticate'; +export const FLOW_REGISTER = 'register'; + +// adapted from https://stackoverflow.com/a/21797381/8204697 +function base64ToBuffer(base64) { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i += 1) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + +// adapted from https://stackoverflow.com/a/9458996/8204697 +function bufferToBase64(buffer) { + if (typeof buffer === 'string') { + return buffer; + } + + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i += 1) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); +} + +/** + * Returns a copy of the given object with the id property converted to buffer + * + * @param {Object} param + */ +function convertIdToBuffer({ id, ...rest }) { + return { + ...rest, + id: base64ToBuffer(id), + }; +} + +/** + * Returns a copy of the given array with all `id`s of the items converted to buffer + * + * @param {Array} items + */ +function convertIdsToBuffer(items) { + return items.map(convertIdToBuffer); +} + +/** + * Returns an object with keys of the given props, and values from the given object converted to base64 + * + * @param {String} obj + * @param {Array} props + */ +function convertPropertiesToBase64(obj, props) { + return props.reduce( + (acc, property) => Object.assign(acc, { [property]: bufferToBase64(obj[property]) }), + {}, + ); +} + +export function convertGetParams({ allowCredentials, challenge, ...rest }) { + return { + ...rest, + ...(allowCredentials ? { allowCredentials: convertIdsToBuffer(allowCredentials) } : {}), + challenge: base64ToBuffer(challenge), + }; +} + +export function convertGetResponse(webauthnResponse) { + return { + type: webauthnResponse.type, + id: webauthnResponse.id, + rawId: bufferToBase64(webauthnResponse.rawId), + response: convertPropertiesToBase64(webauthnResponse.response, [ + 'clientDataJSON', + 'authenticatorData', + 'signature', + 'userHandle', + ]), + clientExtensionResults: webauthnResponse.getClientExtensionResults(), + }; +} + +export function convertCreateParams({ challenge, user, excludeCredentials, ...rest }) { + return { + ...rest, + challenge: base64ToBuffer(challenge), + user: convertIdToBuffer(user), + ...(excludeCredentials ? { excludeCredentials: convertIdsToBuffer(excludeCredentials) } : {}), + }; +} + +export function convertCreateResponse(webauthnResponse) { + return { + type: webauthnResponse.type, + id: webauthnResponse.id, + rawId: bufferToBase64(webauthnResponse.rawId), + clientExtensionResults: webauthnResponse.getClientExtensionResults(), + response: convertPropertiesToBase64(webauthnResponse.response, [ + 'clientDataJSON', + 'attestationObject', + ]), + }; +} diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index e3e140ea35e..fded391cc84 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -2,7 +2,14 @@ /* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; import { mapActions, mapGetters } from 'vuex'; -import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { + GlDeprecatedButton, + GlTooltipDirective, + GlSafeHtmlDirective, + GlLoadingIcon, + GlIcon, + GlButton, +} from '@gitlab/ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { truncateSha } from '~/lib/utils/text_utility'; @@ -21,9 +28,11 @@ export default { GlIcon, FileIcon, DiffStats, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, props: { discussionPath: { @@ -77,6 +86,21 @@ export default { return this.discussionPath; }, + submoduleDiffCompareLinkText() { + if (this.diffFile.submodule_compare) { + const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha)); + const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha)); + return sprintf( + s__('Compare %{oldCommitId}...%{newCommitId}'), + { + oldCommitId: `${truncatedOldSha}`, + newCommitId: `${truncatedNewSha}`, + }, + false, + ); + } + return null; + }, filePath() { if (this.diffFile.submodule) { return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`; @@ -311,5 +335,18 @@ export default { + +
+ +
diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue index 367ed6ca22a..1a2202b23c8 100644 --- a/app/assets/javascripts/packages/details/components/dependency_row.vue +++ b/app/assets/javascripts/packages/details/components/dependency_row.vue @@ -26,7 +26,7 @@ export default {
{{ dependency.version_pattern }} diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index b35f0cbec2e..983062c79f1 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -1,5 +1,5 @@