mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	 ca14c1919f
			
		
	
	ca14c1919f
	
	
	
		
			
			* 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 31430df7bb42580a976d082667cb6ed1f09c3944. * 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');
 | |
|   });
 | |
| });
 |