diff --git a/ui/app/adapters/mfa-setup.js b/ui/app/adapters/mfa-setup.js new file mode 100644 index 0000000000000..c22561ef28005 --- /dev/null +++ b/ui/app/adapters/mfa-setup.js @@ -0,0 +1,13 @@ +import ApplicationAdapter from './application'; + +export default class MfaSetupAdapter extends ApplicationAdapter { + adminGenerate(data) { + let url = `/v1/identity/mfa/method/totp/admin-generate`; + return this.ajax(url, 'POST', { data }); + } + + adminDestroy(data) { + let url = `/v1/identity/mfa/method/totp/admin-destroy`; + return this.ajax(url, 'POST', { data }); + } +} diff --git a/ui/app/components/auth-info.js b/ui/app/components/auth-info.js index 1d52f69adad07..e4a1705b444fd 100644 --- a/ui/app/components/auth-info.js +++ b/ui/app/components/auth-info.js @@ -20,8 +20,13 @@ export default class AuthInfoComponent extends Component { @service wizard; @service router; - @tracked - fakeRenew = false; + @tracked fakeRenew = false; + + get hasEntityId() { + // root users will not have an entity_id because they are not associated with an entity. + // in order to use the MFA end user setup they need an entity_id + return this.auth.authData.entity_id ? true : false; + } get isRenewing() { return this.fakeRenew || this.auth.isRenewing; diff --git a/ui/app/components/mfa-setup-step-one.js b/ui/app/components/mfa-setup-step-one.js new file mode 100644 index 0000000000000..52b533b8c95ab --- /dev/null +++ b/ui/app/components/mfa-setup-step-one.js @@ -0,0 +1,77 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +/** + * @module MfaSetupStepOne + * MfaSetupStepOne component is a child component used in the end user setup for MFA. It records the UUID (aka method_id) and sends a admin-generate request. + * + * @param {string} entityId - the entityId of the user. This comes from the auth service which records it on loading of the cluster. A root user does not have an entityId. + * @param {function} isUUIDVerified - a function that consumes a boolean. Is true if the admin-generate is successful and false if it throws a warning or error. + * @param {boolean} restartFlow - a boolean that is true that is true if the user should proceed to step two or false if they should stay on step one. + * @param {function} saveUUIDandQrCode - A function that sends the inputted UUID and return qrCode from step one to the parent. + * @param {boolean} showWarning - whether a warning is returned from the admin-generate query. Needs to be passed to step two. + */ + +export default class MfaSetupStepOne extends Component { + @service store; + @tracked error = ''; + @tracked warning = ''; + @tracked qrCode = ''; + + @action + redirectPreviousPage() { + this.args.restartFlow(); + window.history.back(); + } + + @action + async verifyUUID(evt) { + evt.preventDefault(); + let response = await this.postAdminGenerate(); + + if (response === 'stop_progress') { + this.args.isUUIDVerified(false); + } else if (response === 'reset_method') { + this.args.showWarning(this.warning); + } else { + this.args.isUUIDVerified(true); + } + } + + async postAdminGenerate() { + this.error = ''; + this.warning = ''; + let adapter = this.store.adapterFor('mfa-setup'); + let response; + + try { + response = await adapter.adminGenerate({ + entity_id: this.args.entityId, + method_id: this.UUID, // comes from value on the input + }); + this.args.saveUUIDandQrCode(this.UUID, response.data?.url); + // if there was a warning it won't fail but needs to be handled here and the flow needs to be interrupted + let warnings = response.warnings || []; + if (warnings.length > 0) { + this.UUID = ''; // clear UUID + const alreadyGenerated = warnings.find((w) => + w.includes('Entity already has a secret for MFA method') + ); + if (alreadyGenerated) { + this.warning = + 'A QR code has already been generated, scanned, and MFA set up for this entity. If a new code is required, contact your administrator.'; + return 'reset_method'; + } + this.warning = warnings; // in case other kinds of warnings comes through. + return 'reset_method'; + } + } catch (error) { + this.UUID = ''; // clear the UUID + this.error = error.errors; + return 'stop_progress'; + } + return response; + } +} diff --git a/ui/app/components/mfa-setup-step-two.js b/ui/app/components/mfa-setup-step-two.js new file mode 100644 index 0000000000000..d81765160ca47 --- /dev/null +++ b/ui/app/components/mfa-setup-step-two.js @@ -0,0 +1,40 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +/** + * @module MfaSetupStepTwo + * MfaSetupStepTwo component is a child component used in the end user setup for MFA. It displays a qrCode or a warning and allows a user to reset the method. + * + * @param {string} entityId - the entityId of the user. This comes from the auth service which records it on loading of the cluster. A root user does not have an entityId. + * @param {string} uuid - the UUID that is entered in the input on step one. + * @param {string} qrCode - the returned url from the admin-generate post. Used to create the qrCode. + * @param {boolean} restartFlow - a boolean that is true that is true if the user should proceed to step two or false if they should stay on step one. + * @param {string} warning - if there is a warning returned from the admin-generate post then it's sent to the step two component in this param. + */ + +export default class MfaSetupStepTwo extends Component { + @service store; + + @action + redirectPreviousPage() { + this.args.restartFlow(); + window.history.back(); + } + + @action + async restartSetup() { + this.error = null; + let adapter = this.store.adapterFor('mfa-setup'); + try { + await adapter.adminDestroy({ + entity_id: this.args.entityId, + method_id: this.args.uuid, + }); + } catch (error) { + this.error = error.errors; + return 'stop_progress'; + } + this.args.restartFlow(); + } +} diff --git a/ui/app/components/splash-page.js b/ui/app/components/splash-page.js index a8966ae67d63b..5942d3ab8187b 100644 --- a/ui/app/components/splash-page.js +++ b/ui/app/components/splash-page.js @@ -7,6 +7,7 @@ export default Component.extend({ auth: service(), store: service(), tagName: '', + showTruncatedNavBar: true, activeCluster: computed('auth.activeCluster', function () { return this.store.peekRecord('cluster', this.auth.activeCluster); diff --git a/ui/app/controllers/vault/cluster/mfa-setup.js b/ui/app/controllers/vault/cluster/mfa-setup.js new file mode 100644 index 0000000000000..54c250cc17184 --- /dev/null +++ b/ui/app/controllers/vault/cluster/mfa-setup.js @@ -0,0 +1,43 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class VaultClusterMfaSetupController extends Controller { + @service auth; + @tracked onStep = 1; + @tracked warning = ''; + @tracked uuid = ''; + @tracked qrCode = ''; + + get entityId() { + return this.auth.authData.entity_id; + } + + @action isUUIDVerified(verified) { + this.warning = ''; // clear the warning, otherwise it persists. + if (verified) { + this.onStep = 2; + } else { + this.restartFlow(); + } + } + + @action + restartFlow() { + this.onStep = 1; + } + + @action + saveUUIDandQrCode(uuid, qrCode) { + // qrCode could be an empty string if the admin-generate was not successful + this.uuid = uuid; + this.qrCode = qrCode; + } + + @action + showWarning(warning) { + this.warning = warning; + this.onStep = 2; + } +} diff --git a/ui/app/router.js b/ui/app/router.js index c0a2ab543bbcb..3cd477670a0ff 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -17,6 +17,7 @@ Router.map(function () { this.route('logout'); this.mount('open-api-explorer', { path: '/api-explorer' }); this.route('license'); + this.route('mfa-setup'); this.route('clients', function () { this.route('current'); this.route('history'); diff --git a/ui/app/routes/vault/cluster/mfa-setup.js b/ui/app/routes/vault/cluster/mfa-setup.js new file mode 100644 index 0000000000000..e8f10fa096ac7 --- /dev/null +++ b/ui/app/routes/vault/cluster/mfa-setup.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class MfaSetupRoute extends Route {} diff --git a/ui/app/styles/components/list-item-row.scss b/ui/app/styles/components/list-item-row.scss index 2558358990b05..75cad84fb957b 100644 --- a/ui/app/styles/components/list-item-row.scss +++ b/ui/app/styles/components/list-item-row.scss @@ -16,6 +16,12 @@ font-weight: $font-weight-semibold; color: $ui-gray-500; } + + .center-display { + width: 50%; + margin-left: auto; + margin-right: auto; + } } a.list-item-row, diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index 123a0f594efdb..031ee33e980f9 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -183,6 +183,10 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); font-weight: $font-weight-semibold; } + &.has-text-danger { + border: 1px solid $red-500; + } + &.tool-tip-trigger { color: $grey-dark; min-width: auto; diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss index f74971d66720a..8e836582bde25 100644 --- a/ui/app/styles/core/helpers.scss +++ b/ui/app/styles/core/helpers.scss @@ -81,6 +81,10 @@ .is-flex-start { display: flex !important; justify-content: flex-start; + + &.has-gap { + gap: $spacing-m; + } } .is-flex-full { flex-basis: 100%; diff --git a/ui/app/templates/components/auth-info.hbs b/ui/app/templates/components/auth-info.hbs index ce7a77c9e8e66..ab9592cca0e5e 100644 --- a/ui/app/templates/components/auth-info.hbs +++ b/ui/app/templates/components/auth-info.hbs @@ -16,6 +16,13 @@ /> {{/if}} + {{#if this.hasEntityId}} +
  • + + Multi-factor authentication + +
  • + {{/if}}
  • + + + \ No newline at end of file diff --git a/ui/app/templates/components/mfa-setup-step-two.hbs b/ui/app/templates/components/mfa-setup-step-two.hbs new file mode 100644 index 0000000000000..d6168bf6650b7 --- /dev/null +++ b/ui/app/templates/components/mfa-setup-step-two.hbs @@ -0,0 +1,35 @@ +

    + TOTP Multi-factor authentication (MFA) can be enabled here if it is required by your administrator. This will ensure that + you are not prevented from logging into Vault in the future, once MFA is fully enforced. +

    +
    + + {{#if @warning}} + + {{else}} +
    +
    + {{! template-lint-disable no-curly-component-invocation }} + {{qr-code text=@qrCode colorLight="#F7F7F7" width=155 height=155 correctLevel="L"}} +
    +
    +
    +
    +
    +

    + After you leave this page, this QR code will be removed and + cannot + be regenerated. +

    +
    +
    + {{/if}} +
    + + +
    +
    \ No newline at end of file diff --git a/ui/app/templates/components/splash-page.hbs b/ui/app/templates/components/splash-page.hbs index 36f115a5d62b6..9070e5f04e21d 100644 --- a/ui/app/templates/components/splash-page.hbs +++ b/ui/app/templates/components/splash-page.hbs @@ -1,15 +1,17 @@ - - - - - - - - - - +{{#if this.showTruncatedNavBar}} + + + + + + + + + + +{{/if}} {{! bypass UiWizard and container styling }} {{#if this.hasAltContent}} {{yield (hash altContent=(component "splash-page/splash-content"))}} diff --git a/ui/app/templates/vault/cluster/mfa-setup.hbs b/ui/app/templates/vault/cluster/mfa-setup.hbs new file mode 100644 index 0000000000000..a91cc93d47cde --- /dev/null +++ b/ui/app/templates/vault/cluster/mfa-setup.hbs @@ -0,0 +1,29 @@ + + +

    MFA setup

    +
    + +
    +
    + {{#if (eq this.onStep 1)}} + + {{/if}} + {{#if (eq this.onStep 2)}} + + {{/if}} +
    +
    +
    +
    \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index 5ea7fae101ba1..a3f14fe3ca5c5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -124,6 +124,7 @@ "ember-modifier": "^3.1.0", "ember-page-title": "^6.2.2", "ember-power-select": "^5.0.3", + "ember-qrcode-shim": "^0.4.0", "ember-qunit": "^5.1.5", "ember-resolver": "^8.0.3", "ember-responsive": "^3.0.0-beta.3", diff --git a/ui/yarn.lock b/ui/yarn.lock index 4b18eade92ca0..cda6465c1ecac 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -8327,6 +8327,13 @@ ember-power-select@^5.0.3: ember-text-measurer "^0.6.0" ember-truth-helpers "^2.1.0 || ^3.0.0" +ember-qrcode-shim@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/ember-qrcode-shim/-/ember-qrcode-shim-0.4.0.tgz#bc4c61e8c33c7e731e98d68780a772d59eec4fc6" + integrity sha512-tmdxr7mqfeG5vK6Lb553qmFlhnZipZyGBPQIBh5TbRQozPH5ATVS7zq77eV//d9y3997R7hGIYTNbsGZ718lOw== + dependencies: + ember-cli-babel "^7.1.2" + ember-qunit@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-5.1.5.tgz#24a7850f052be24189ff597dfc31b923e684c444"