open-vault/ui/app/components/auth-jwt.js
Jordan Reimer 5c2a08de6d
Ember Upgrade to 3.24 (#13443)
* Update browserslist

* Add browserslistrc

* ember-cli-update --to 3.26, fix conflicts

* Run codemodes that start with ember-*

* More codemods - before cp*

* More codemods (curly data-test-*)

* WIP ember-basic-dropdown template errors

* updates ember-basic-dropdown and related deps to fix build issues

* updates basic dropdown instances to new version API

* updates more deps -- ember-template-lint is working again

* runs no-implicit-this codemod

* creates and runs no-quoteless-attributes codemod

* runs angle brackets codemod

* updates lint:hbs globs to only touch hbs files

* removes yield only templates

* creates and runs deprecated args transform

* supresses lint error for invokeAction on LinkTo component

* resolves remaining ambiguous path lint errors

* resolves simple-unless lint errors

* adds warnings for deprecated tagName arg on LinkTo components

* adds warnings for remaining curly component invocation

* updates global template lint rules

* resolves remaining template lint errors

* disables some ember specfic lint rules that target pre octane patterns

* js lint fix run

* resolves remaining js lint errors

* fixes test run

* adds npm-run-all dep

* fixes test attribute issues

* fixes console acceptance tests

* fixes tests

* adds yield only wizard/tutorial-active template

* fixes more tests

* attempts to fix more flaky tests

* removes commented out settled in transit test

* updates deprecations workflow and adds initializer to filter by version

* updates flaky policies acl old test

* updates to flaky transit test

* bumps ember deps down to LTS version

* runs linters after main merge

* fixes client count tests after bad merge conflict fixes

* fixes client count history test

* more updates to lint config

* another round of hbs lint fixes after extending stylistic rule

* updates lint-staged commands

* removes indent eslint rule since it seems to break things

* fixes bad attribute in transform-edit-form template

* test fixes

* fixes enterprise tests

* adds changelog

* removes deprecated ember-concurrency-test-waiters dep and adds @ember/test-waiters

* flaky test fix

Co-authored-by: hashishaw <cshaw@hashicorp.com>
2021-12-16 20:44:29 -07:00

200 lines
6.4 KiB
JavaScript

import Ember from 'ember';
import { inject as service } from '@ember/service';
import Component from './outer-html';
import { later } from '@ember/runloop';
import { task, timeout, waitForEvent } from 'ember-concurrency';
import { computed } from '@ember/object';
import { waitFor } from '@ember/test-waiters';
const WAIT_TIME = 500;
const ERROR_WINDOW_CLOSED =
'The provider window was closed before authentication was complete. Please click Sign In to try again.';
const ERROR_MISSING_PARAMS =
'The callback from the provider did not supply all of the required parameters. Please click Sign In to try again. If the problem persists, you may want to contact your administrator.';
const ERROR_JWT_LOGIN = 'OIDC login is not configured for this mount';
export { ERROR_WINDOW_CLOSED, ERROR_MISSING_PARAMS, ERROR_JWT_LOGIN };
export default Component.extend({
store: service(),
featureFlagService: service('featureFlag'),
selectedAuthPath: null,
selectedAuthType: null,
roleName: null,
role: null,
errorMessage: null,
onRoleName() {},
onLoading() {},
onError() {},
onToken() {},
onNamespace() {},
didReceiveAttrs() {
this._super();
let { oldSelectedAuthPath, selectedAuthPath } = this;
let shouldDebounce = !oldSelectedAuthPath && !selectedAuthPath;
if (oldSelectedAuthPath !== selectedAuthPath) {
this.set('role', null);
this.onRoleName(this.roleName);
this.fetchRole.perform(null, { debounce: false });
} else if (shouldDebounce) {
this.fetchRole.perform(this.roleName);
}
this.set('errorMessage', null);
this.set('oldSelectedAuthPath', selectedAuthPath);
},
// Assumes authentication using OIDC until it's known that the mount is
// configured for JWT authentication via static keys, JWKS, or OIDC discovery.
isOIDC: computed('errorMessage', function () {
return this.errorMessage !== ERROR_JWT_LOGIN;
}),
getWindow() {
return this.window || window;
},
fetchRole: task(
waitFor(function* (roleName, options = { debounce: true }) {
if (options.debounce) {
this.onRoleName(roleName);
// debounce
yield timeout(Ember.testing ? 0 : WAIT_TIME);
}
let path = this.selectedAuthPath || this.selectedAuthType;
let id = JSON.stringify([path, roleName]);
let role = null;
try {
role = yield this.store.findRecord('role-jwt', id, { adapterOptions: { namespace: this.namespace } });
} catch (e) {
// throwing here causes failures in tests
if ((!e.httpStatus || e.httpStatus !== 400) && !Ember.testing) {
throw e;
}
if (e.errors && e.errors.length > 0) {
this.set('errorMessage', e.errors[0]);
}
}
this.set('role', role);
})
).restartable(),
handleOIDCError(err) {
this.onLoading(false);
this.prepareForOIDC.cancelAll();
this.onError(err);
},
prepareForOIDC: task(function* (oidcWindow) {
const thisWindow = this.getWindow();
// show the loading animation in the parent
this.onLoading(true);
// start watching the popup window and the current one
this.watchPopup.perform(oidcWindow);
this.watchCurrent.perform(oidcWindow);
// wait for message posted from oidc callback
// see issue https://github.com/hashicorp/vault/issues/12436
// ensure that postMessage event is from expected source
while (true) {
const event = yield waitForEvent(thisWindow, 'message');
if (event.origin !== thisWindow.origin || !event.isTrusted) {
return this.handleOIDCError();
}
if (event.data.source === 'oidc-callback') {
return this.exchangeOIDC.perform(event.data, oidcWindow);
}
// continue to wait for the correct message
}
}),
watchPopup: task(function* (oidcWindow) {
while (true) {
yield timeout(WAIT_TIME);
if (!oidcWindow || oidcWindow.closed) {
return this.handleOIDCError(ERROR_WINDOW_CLOSED);
}
}
}),
watchCurrent: task(function* (oidcWindow) {
// when user is about to change pages, close the popup window
yield waitForEvent(this.getWindow(), 'beforeunload');
oidcWindow.close();
}),
closeWindow(oidcWindow) {
this.watchPopup.cancelAll();
this.watchCurrent.cancelAll();
oidcWindow.close();
},
exchangeOIDC: task(function* (oidcState, oidcWindow) {
if (oidcState === null || oidcState === undefined) {
return;
}
this.onLoading(true);
let { namespace, path, state, code } = oidcState;
// The namespace can be either be passed as a query paramter, or be embedded
// in the state param in the format `<state_id>,ns=<namespace>`. So if
// `namespace` is empty, check for namespace in state as well.
if (namespace === '' || this.featureFlagService.managedNamespaceRoot) {
let i = state.indexOf(',ns=');
if (i >= 0) {
// ",ns=" is 4 characters
namespace = state.substring(i + 4);
state = state.substring(0, i);
}
}
// defer closing of the window, but continue executing the task
later(() => {
this.closeWindow(oidcWindow);
}, WAIT_TIME);
if (!path || !state || !code) {
return this.handleOIDCError(ERROR_MISSING_PARAMS);
}
let adapter = this.store.adapterFor('auth-method');
this.onNamespace(namespace);
let resp;
// do the OIDC exchange, set the token on the parent component
// and submit auth form
try {
resp = yield adapter.exchangeOIDC(path, state, code);
} catch (e) {
return this.handleOIDCError(e);
}
let token = resp.auth.client_token;
this.onSelectedAuth('token');
this.onToken(token);
yield this.onSubmit();
}),
actions: {
async startOIDCAuth(data, e) {
this.onError(null);
if (e && e.preventDefault) {
e.preventDefault();
}
if (!this.isOIDC || !this.role || !this.role.authUrl) {
return;
}
await this.fetchRole.perform(this.roleName, { debounce: false });
let win = this.getWindow();
const POPUP_WIDTH = 500;
const POPUP_HEIGHT = 600;
let left = win.screen.width / 2 - POPUP_WIDTH / 2;
let top = win.screen.height / 2 - POPUP_HEIGHT / 2;
let oidcWindow = win.open(
this.role.authUrl,
'vaultOIDCWindow',
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},resizable,scrollbars=yes,top=${top},left=${left}`
);
this.prepareForOIDC.perform(oidcWindow);
},
},
});