Skip to content

Commit

Permalink
MFA Config (#15200)
Browse files Browse the repository at this point in the history
* 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 31430df.

* 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>
  • Loading branch information
5 people committed May 21, 2022
1 parent 38abe20 commit ca14c19
Show file tree
Hide file tree
Showing 92 changed files with 3,141 additions and 179 deletions.
29 changes: 29 additions & 0 deletions ui/app/adapters/mfa-login-enforcement.js
@@ -0,0 +1,29 @@
import ApplicationAdapter from './application';

export default class KeymgmtKeyAdapter extends ApplicationAdapter {
namespace = 'v1';

pathForType() {
return 'identity/mfa/login-enforcement';
}

_saveRecord(store, { modelName }, snapshot) {
const data = store.serializerFor(modelName).serialize(snapshot);
return this.ajax(this.urlForUpdateRecord(snapshot.attr('name'), modelName, snapshot), 'POST', {
data,
}).then(() => data);
}
// create does not return response similar to PUT request
createRecord() {
return this._saveRecord(...arguments);
}
// update record via POST method
updateRecord() {
return this._saveRecord(...arguments);
}

query(store, type, query) {
const url = this.urlForQuery(query, type.modelName);
return this.ajax(url, 'GET', { data: { list: true } });
}
}
54 changes: 54 additions & 0 deletions ui/app/adapters/mfa-method.js
@@ -0,0 +1,54 @@
import ApplicationAdapter from './application';

export default class MfaMethodAdapter extends ApplicationAdapter {
namespace = 'v1';

pathForType() {
return 'identity/mfa/method';
}

createOrUpdate(store, type, snapshot) {
const data = store.serializerFor(type.modelName).serialize(snapshot);
const { id } = snapshot;
return this.ajax(this.buildURL(type.modelName, id, snapshot, 'POST'), 'POST', {
data,
}).then((res) => {
// TODO: Check how 204's are handled by ember
return {
data: {
...data,
id: res?.data?.method_id || id,
},
};
});
}

createRecord() {
return this.createOrUpdate(...arguments);
}

updateRecord() {
return this.createOrUpdate(...arguments);
}

urlForDeleteRecord(id, modelName, snapshot) {
return this.buildURL(modelName, id, snapshot, 'POST');
}

query(store, type, query) {
const url = this.urlForQuery(query, type.modelName);
return this.ajax(url, 'GET', {
data: {
list: true,
},
});
}

buildURL(modelName, id, snapshot, requestType) {
if (requestType === 'POST') {
let url = `${super.buildURL(modelName)}/${snapshot.attr('type')}`;
return id ? `${url}/${id}` : url;
}
return super.buildURL(...arguments);
}
}
13 changes: 13 additions & 0 deletions 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 });
}
}
9 changes: 7 additions & 2 deletions ui/app/components/auth-info.js
Expand Up @@ -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;
}

get isRenewing() {
return this.fakeRenew || this.auth.isRenewing;
Expand Down
38 changes: 29 additions & 9 deletions ui/app/components/mfa-form.js
Expand Up @@ -15,16 +15,29 @@ import { numberToWord } from 'vault/helpers/number-to-word';
* @param {string} clusterId - id of selected cluster
* @param {object} authData - data from initial auth request -- { mfa_requirement, backend, data }
* @param {function} onSuccess - fired when passcode passes validation
* @param {function} onError - fired for multi-method or non-passcode method validation errors
*/

export const VALIDATION_ERROR =
export const TOTP_VALIDATION_ERROR =
'The passcode failed to validate. If you entered the correct passcode, contact your administrator.';

export default class MfaForm extends Component {
@service auth;

@tracked countdown;
@tracked error;
@tracked codeDelayMessage;

constructor() {
super(...arguments);
// trigger validation immediately when passcode is not required
const passcodeOrSelect = this.constraints.filter((constraint) => {
return constraint.methods.length > 1 || constraint.methods.findBy('uses_passcode');
});
if (!passcodeOrSelect.length) {
this.validate.perform();
}
}

get constraints() {
return this.args.authData.mfa_requirement.mfa_constraints;
Expand Down Expand Up @@ -66,19 +79,26 @@ export default class MfaForm extends Component {
});
this.args.onSuccess(response);
} catch (error) {
const codeUsed = (error.errors || []).find((e) => e.includes('code already used;'));
if (codeUsed) {
// parse validity period from error string to initialize countdown
const seconds = parseInt(codeUsed.split('in ')[1].split(' seconds')[0]);
this.newCodeDelay.perform(seconds);
const errors = error.errors || [];
const codeUsed = errors.find((e) => e.includes('code already used'));
const rateLimit = errors.find((e) => e.includes('maximum TOTP validation attempts'));
const delayMessage = codeUsed || rateLimit;

if (delayMessage) {
const reason = codeUsed ? 'This code has already been used' : 'Maximum validation attempts exceeded';
this.codeDelayMessage = `${reason}. Please wait until a new code is available.`;
this.newCodeDelay.perform(delayMessage);
} else if (this.singlePasscode) {
this.error = TOTP_VALIDATION_ERROR;
} else {
this.error = VALIDATION_ERROR;
this.args.onError(this.auth.handleError(error));
}
}
}

@task *newCodeDelay(timePeriod) {
this.countdown = timePeriod;
@task *newCodeDelay(message) {
// parse validity period from error string to initialize countdown
this.countdown = parseInt(message.match(/(\d\w seconds)/)[0].split(' ')[0]);
while (this.countdown) {
yield timeout(1000);
this.countdown--;
Expand Down
170 changes: 170 additions & 0 deletions ui/app/components/mfa-login-enforcement-form.js
@@ -0,0 +1,170 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';

/**
* @module MfaLoginEnforcementForm
* MfaLoginEnforcementForm components are used to create and edit login enforcements
*
* @example
* ```js
* <MfaLoginEnforcementForm @model={{this.model}} @isInline={{false}} @onSave={{this.onSave}} @onClose={{this.onClose}} />
* ```
* @callback onSave
* @callback onClose
* @param {Object} model - login enforcement model
* @param {Object} [isInline] - toggles inline display of form -- method selector and actions are hidden and should be handled externally
* @param {Object} [modelErrors] - model validations state object if handling actions externally when displaying inline
* @param {onSave} [onSave] - triggered on save success
* @param {onClose} [onClose] - triggered on cancel
*/

export default class MfaLoginEnforcementForm extends Component {
@service store;
@service flashMessages;

targetTypes = [
{ label: 'Authentication mount', type: 'accessor', key: 'auth_method_accessors' },
{ label: 'Authentication method', type: 'method', key: 'auth_method_types' },
{ label: 'Group', type: 'identity/group', key: 'identity_groups' },
{ label: 'Entity', type: 'identity/entity', key: 'identity_entities' },
];
searchSelectOptions = null;

@tracked name;
@tracked targets = [];
@tracked selectedTargetType = 'accessor';
@tracked selectedTargetValue = null;
@tracked searchSelect = {
options: [],
selected: [],
};
@tracked authMethods = [];
@tracked modelErrors;

constructor() {
super(...arguments);
// aggregate different target array properties on model into flat list
this.flattenTargets();
// eagerly fetch identity groups and entities for use as search select options
this.resetTargetState();
// only auth method types that have mounts can be selected as targets -- fetch from sys/auth and map by type
this.fetchAuthMethods();
}

async flattenTargets() {
for (let { label, key } of this.targetTypes) {
const targetArray = await this.args.model[key];
const targets = targetArray.map((value) => ({ label, key, value }));
this.targets.addObjects(targets);
}
}
async resetTargetState() {
this.selectedTargetValue = null;
const options = this.searchSelectOptions || {};
if (!this.searchSelectOptions) {
const types = ['identity/group', 'identity/entity'];
for (const type of types) {
try {
options[type] = (await this.store.query(type, {})).toArray();
} catch (error) {
options[type] = [];
}
}
this.searchSelectOptions = options;
}
if (this.selectedTargetType.includes('identity')) {
this.searchSelect = {
selected: [],
options: [...options[this.selectedTargetType]],
};
}
}
async fetchAuthMethods() {
const mounts = (await this.store.findAll('auth-method')).toArray();
this.authMethods = mounts.mapBy('type');
}

get selectedTarget() {
return this.targetTypes.findBy('type', this.selectedTargetType);
}
get errors() {
return this.args.modelErrors || this.modelErrors;
}

@task
*save() {
this.modelErrors = {};
// check validity state first and abort if invalid
const { isValid, state } = this.args.model.validate();
if (!isValid) {
this.modelErrors = state;
} else {
try {
yield this.args.model.save();
this.args.onSave();
} catch (error) {
const message = error.errors ? error.errors.join('. ') : error.message;
this.flashMessages.danger(message);
}
}
}

@action
async onMethodChange(selectedIds) {
const methods = await this.args.model.mfa_methods;
// first check for existing methods that have been removed from selection
methods.forEach((method) => {
if (!selectedIds.includes(method.id)) {
methods.removeObject(method);
}
});
// now check for selected items that don't exist and add them to the model
const methodIds = methods.mapBy('id');
selectedIds.forEach((id) => {
if (!methodIds.includes(id)) {
const model = this.store.peekRecord('mfa-method', id);
methods.addObject(model);
}
});
}
@action
onTargetSelect(type) {
this.selectedTargetType = type;
this.resetTargetState();
}
@action
setTargetValue(selected) {
const { type } = this.selectedTarget;
if (type.includes('identity')) {
// for identity groups and entities grab model from store as value
this.selectedTargetValue = this.store.peekRecord(type, selected[0]);
} else {
this.selectedTargetValue = selected;
}
}
@action
addTarget() {
const { label, key } = this.selectedTarget;
const value = this.selectedTargetValue;
this.targets.addObject({ label, value, key });
// add target to appropriate model property
this.args.model[key].addObject(value);
this.selectedTargetValue = null;
this.resetTargetState();
}
@action
removeTarget(target) {
this.targets.removeObject(target);
// remove target from appropriate model property
this.args.model[target.key].removeObject(target.value);
}
@action
cancel() {
// revert model changes
this.args.model.rollbackAttributes();
this.args.onClose();
}
}

0 comments on commit ca14c19

Please sign in to comment.