vault/ui/tests/integration/components/mfa-form-test.js
Jordan Reimer ca14c1919f
MFA Config (#15200)
* adds mirage factories for mfa methods and login enforcement

* adds mirage handler for mfa config endpoints

* adds mirage identity manager for uuids

* updates mfa test to use renamed mfaLogin mirage handler

* updates mfa login workflow for push methods (#15214)

* MFA Login Enforcement Model (#15244)

* adds mfa login enforcement model, adapter and serializer

* updates mfa methods to hasMany realtionship and transforms property names

* updates login enforcement adapter to use urlForQuery over buildURL

* Model for mfa method (#15218)

* Model for mfa method

* Added adapter and serializer for mfa method

- Updated mfa method model
- Basic route to handle list view
- Added MFA to access nav

* Show landing page if methods are not configured

* Updated adapter,serializer

- Backend is adding new endpoint to list all the mfa methods

* Updated landing page

- Added MFA diagram
- Created helper to resolve full path for assets like images

* Remove ember assign

* Fixed failing test

* MFA method and enforcement list view (#15353)

* MFA method and enforcement list view

- Added new route for list views
- List mfa methods along with id, type and icon
- Added client side pagination to list views

* Throw error if method id is not present

* MFA Login Enforcement Form (#15410)

* adds mfa login enforcement form and header components and radio card component

* skips login enforcement form tests for now

* adds jsdoc annotations for mfa-login-enforcement-header component

* adds error handling when fetching identity targets in login enforcement form component

* updates radio-card label elements

* MFA Login Enforcement Create and Edit routes (#15422)

* adds mfa login enforcement form and header components and radio card component

* skips login enforcement form tests for now

* updates to login enforcement form to fix issues hydrating methods and targets from model when editing

* updates to mfa-config mirage handler and login enforcement handler

* fixes issue with login enforcement serializer normalizeItems method throwing error on save

* updates to mfa route structure

* adds login enforcement create and edit routes

* MFA Login Enforcement Read Views (#15462)

* adds login enforcement read views

* skip mfa-method-list-item test for now

* MFA method form (#15432)

* MFA method form

- Updated model for form attributes
- Form for editing, creating mfa methods

* Added comments

* Update model for mfa method

* Refactor buildURL in mfa method adapter

* Update adapter to handle mfa create

* Fixed adapter to handle create mfa response

* Sidebranch: MFA end user setup (#15273)

* initial setup of components and route

* fix navbar

* replace parent component with controller

* use auth service to return entity id

* adapter and some error handling:

* clean up adapter and handle warning

* wip

* use library for qrCode generation

* clear warning and QR code display fix

* flow for restart setup

* add documentation

* clean up

* fix warning issue

* handle root user

* remove comment

* update copy

* fix margin

* address comment

* MFA Guided Setup Route (#15479)

* adds mfa method create route with type selection workflow

* updates mfa method create route links to use DocLink component

* MFA Guided Setup Config View (#15486)

* adds mfa guided setup config view

* resets type query param on mfa method create route exit

* hide next button if type is not selected in mfa method create route

* updates to sure correct state when changing mfa method type in guided setup

* Enforcement view at MFA method level (#15485)

- List enforcements for each mfa method
- Delete MFA method if no enforcements are present
- Moved method, enforcement list item component to mfa folder

* MFA Login Enforcement Validations (#15498)

* adds model and form validations for mfa login enforcements

* updates mfa login enforcement validation messages

* updates validation message for mfa login enforcement targets

* adds transition action to configure mfa button on landing page

* unset enforcement on preference change in mfa guided setup workflow

* Added validations for mfa method model (#15506)

* UI/mfa breadcrumbs and small fixes (#15499)

* add active class when on index

* breadcrumbs

* remove box-shadow to match designs

* fix refresh load mfa-method

* breadcrumb create

* add an empty state the enforcements list view

* change to beforeModel

* UI/mfa small bugs (#15522)

* remove pagintion and fix on methods list view

* fix enforcements

* Fix label for value on radio-card (#15542)

* MFA Login Enforcement Component Tests (#15539)

* adds tests for mfa-login-enforcement-header component

* adds tests for mfa-login-enforcement-form component

* Remove default values from mfa method model (#15540)

- use passcode had a default value, as a result it was being sent
with all the mfa method types during save and edit flows..

* UI/mfa small cleanup (#15549)

* data-test-mleh -> data-test-mfa

* Only one label per radio card

* Remove unnecessary async

* Simplify boolean logic

* Make mutation clear

* Revert "data-test-mleh -> data-test-mfa"

This reverts commit 31430df7bb.

* updates mfa login enforcement form to only display auth method types for current mounts as targets (#15547)

* remove token type (#15548)

* remove token type

* conditional param

* removes type from mfa method payload and fixes bug transitioning to method route on save success

* removes punctuation from mfa form error message string match

* updates qr-code component invocation to angle bracket

* Re-trigger CI jobs with empty commit

Co-authored-by: Arnav Palnitkar <arnav@hashicorp.com>
Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>
Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
Co-authored-by: Michele Degges <mdeggies@gmail.com>
2022-05-20 18:40:16 -06:00

229 lines
8.4 KiB
JavaScript

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { fillIn, click, waitUntil } from '@ember/test-helpers';
import { _cancelTimers as cancelTimers, later } from '@ember/runloop';
import { TOTP_VALIDATION_ERROR } from 'vault/components/mfa-form';
module('Integration | Component | mfa-form', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.clusterId = '123456';
this.mfaAuthData = {
backend: 'userpass',
data: { username: 'foo', password: 'bar' },
};
this.authService = this.owner.lookup('service:auth');
// setup basic totp mfa_requirement
// override in tests that require different scenarios
this.totpConstraint = this.server.create('mfa-method', { type: 'totp' });
const { mfa_requirement } = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa: { any: [this.totpConstraint] } },
});
this.mfaAuthData.mfa_requirement = mfa_requirement;
});
test('it should render correct descriptions', async function (assert) {
const totpConstraint = this.server.create('mfa-method', { type: 'totp' });
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const duoConstraint = this.server.create('mfa-method', { type: 'duo' });
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [totpConstraint] } },
}).mfa_requirement;
await render(
hbs`<MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
);
assert
.dom('[data-test-mfa-description]')
.includesText(
'Enter your authentication code to log in.',
'Correct description renders for single passcode'
);
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [duoConstraint, oktaConstraint] } },
}).mfa_requirement;
await render(
hbs`<MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
);
assert
.dom('[data-test-mfa-description]')
.includesText(
'Select the MFA method you wish to use.',
'Correct description renders for multiple methods'
);
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [oktaConstraint] }, test_mfa_2: { any: [duoConstraint] } },
}).mfa_requirement;
await render(
hbs`<MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
);
assert
.dom('[data-test-mfa-description]')
.includesText(
'Two methods are required for successful authentication.',
'Correct description renders for multiple constraints'
);
});
test('it should render method selects and passcode inputs', async function (assert) {
assert.expect(2);
const duoConstraint = this.server.create('mfa-method', { type: 'duo', uses_passcode: true });
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const pingidConstraint = this.server.create('mfa-method', { type: 'pingid' });
const { mfa_requirement } = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: {
test_mfa_1: {
any: [pingidConstraint, oktaConstraint],
},
test_mfa_2: {
any: [duoConstraint],
},
},
});
this.mfaAuthData.mfa_requirement = mfa_requirement;
this.server.post('/sys/mfa/validate', (schema, req) => {
const json = JSON.parse(req.requestBody);
const payload = {
mfa_request_id: 'test-mfa-id',
mfa_payload: { [oktaConstraint.id]: [], [duoConstraint.id]: ['test-code'] },
};
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
return {};
});
this.owner.lookup('service:auth').reopen({
// override to avoid authSuccess method since it expects an auth payload
async totpValidate({ mfa_requirement }) {
await this.clusterAdapter().mfaValidate(mfa_requirement);
return 'test response';
},
});
this.onSuccess = (resp) =>
assert.equal(resp, 'test response', 'Response is returned in onSuccess callback');
await render(hbs`
<MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
@onSuccess={{this.onSuccess}}
/>
`);
await fillIn('[data-test-mfa-select="0"] select', oktaConstraint.id);
await fillIn('[data-test-mfa-passcode="1"]', 'test-code');
await click('[data-test-mfa-validate]');
});
test('it should validate mfa requirement', async function (assert) {
assert.expect(5);
this.server.post('/sys/mfa/validate', (schema, req) => {
const json = JSON.parse(req.requestBody);
const payload = {
mfa_request_id: 'test-mfa-id',
mfa_payload: { [this.totpConstraint.id]: ['test-code'] },
};
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
return {};
});
const expectedAuthData = { clusterId: this.clusterId, ...this.mfaAuthData };
this.owner.lookup('service:auth').reopen({
// override to avoid authSuccess method since it expects an auth payload
async totpValidate(authData) {
await waitUntil(() =>
assert.dom('[data-test-mfa-validate]').hasClass('is-loading', 'Loading class applied to button')
);
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled while loading');
assert.deepEqual(authData, expectedAuthData, 'Mfa auth data passed to validate method');
await this.clusterAdapter().mfaValidate(authData.mfa_requirement);
return 'test response';
},
});
this.onSuccess = (resp) =>
assert.equal(resp, 'test response', 'Response is returned in onSuccess callback');
await render(hbs`
<MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
@onSuccess={{this.onSuccess}}
/>
`);
await fillIn('[data-test-mfa-passcode]', 'test-code');
await click('[data-test-mfa-validate]');
});
test('it should show countdown on passcode already used and rate limit errors', async function (assert) {
const messages = {
used: 'code already used; new code is available in 45 seconds',
limit:
'maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Please try again in 15 seconds',
};
const codes = ['used', 'limit'];
for (let code of codes) {
this.owner.lookup('service:auth').reopen({
totpValidate() {
throw { errors: [messages[code]] };
},
});
await render(hbs`
<MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
/>
`);
await fillIn('[data-test-mfa-passcode]', code);
later(() => cancelTimers(), 50);
await click('[data-test-mfa-validate]');
assert
.dom('[data-test-mfa-countdown]')
.hasText(
code === 'used' ? '45' : '15',
'countdown renders with correct initial value from error response'
);
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled during countdown');
assert.dom('[data-test-mfa-passcode]').isDisabled('Input is disabled during countdown');
assert.dom('[data-test-inline-error-message]').exists('Alert message renders');
}
});
test('it should show error message for passcode invalid error', async function (assert) {
this.owner.lookup('service:auth').reopen({
totpValidate() {
throw { errors: ['failed to validate'] };
},
});
await render(hbs`
<MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
/>
`);
await fillIn('[data-test-mfa-passcode]', 'test-code');
later(() => cancelTimers(), 50);
await click('[data-test-mfa-validate]');
assert
.dom('[data-test-error]')
.includesText(TOTP_VALIDATION_ERROR, 'Generic error message renders for passcode validation error');
});
});