mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-12 01:27:01 +02:00
* 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>
229 lines
8.4 KiB
JavaScript
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');
|
|
});
|
|
});
|