mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 15:11:07 +02:00
* change entity_id to camel casing, remove "backends" key from stored auth data * fix tokenExpirationEpoch returning NaN, use authSuccess in auth service tests * camel case mfa_requirement references * refactor auth service * implement api service for token method * implement api service in standard auth methods * add lookupSelf request to persistAuthData method in auht service instead of calling in components * implement api service in oidc-jwt component * implement api service in okta component * implement api service in saml component * use api service for wrapped_token query param * remaining test updates, enterprise tests and stabilize auth helpers * upate renew() to use new persistAuthData method, add a test * revert as this will be addressed upstream * rename supported-login-methods to auth-form-helpers and delete old supported-auth-backends helper, update tests * cleanup normalize after testing mfa validation for each auth method * update type declarations, set displayName in each method component * stabilize redirect tests by waiting for login before asserting url * stabilize tests * modernize typescript syntax, move error const to util * use mirage instead of vault server to resolve test race conditions * fix file import
387 lines
17 KiB
JavaScript
387 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { setupRenderingTest } from 'ember-qunit';
|
|
import { click, fillIn, find, findAll, render } from '@ember/test-helpers';
|
|
import hbs from 'htmlbars-inline-precompile';
|
|
import sinon from 'sinon';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
|
import { AUTH_METHOD_LOGIN_DATA } from 'vault/tests/helpers/auth/auth-helpers';
|
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
|
import { ENTERPRISE_LOGIN_METHODS, ERROR_JWT_LOGIN, supportedTypes } from 'vault/utils/auth-form-helpers';
|
|
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
|
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
|
|
|
|
module('Integration | Component | auth | form template', function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
window.localStorage.clear();
|
|
this.version = this.owner.lookup('service:version');
|
|
this.router = this.owner.lookup('service:router');
|
|
this.cluster = { id: '1' };
|
|
|
|
this.alternateView = null;
|
|
this.defaultView = { view: 'dropdown', tabData: null };
|
|
this.initialFormState = { initialAuthType: 'token', showAlternate: false };
|
|
this.onSuccess = sinon.spy();
|
|
this.visibleMountTypes = null;
|
|
|
|
this.renderComponent = () => {
|
|
return render(hbs`
|
|
<Auth::FormTemplate
|
|
@alternateView={{this.alternateView}}
|
|
@cluster={{this.cluster}}
|
|
@defaultView={{this.defaultView}}
|
|
@initialFormState={{this.initialFormState}}
|
|
@onSuccess={{this.onSuccess}}
|
|
@visibleMountTypes={{this.visibleMountTypes}}
|
|
/>`);
|
|
};
|
|
});
|
|
|
|
// test to select each method is in "ent" module to include enterprise methods
|
|
test('it selects token by default', async function (assert) {
|
|
await this.renderComponent();
|
|
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('token');
|
|
});
|
|
|
|
test('it does not show toggle buttons if @alternateView does not exist', async function (assert) {
|
|
await this.renderComponent();
|
|
assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render');
|
|
assert
|
|
.dom(GENERAL.button('Sign in with other methods'))
|
|
.doesNotExist('"Sign in with other methods" does not render');
|
|
});
|
|
|
|
test('it initializes with preset auth type', async function (assert) {
|
|
this.initialFormState = { initialAuthType: 'userpass' };
|
|
await this.renderComponent();
|
|
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('userpass');
|
|
});
|
|
|
|
test('it displays errors', async function (assert) {
|
|
const api = this.owner.lookup('service:api');
|
|
// stub auth request for "token" method because it's selected by default
|
|
const tokenLookUpSelfStub = sinon.stub(api.auth, 'tokenLookUpSelf');
|
|
tokenLookUpSelfStub.rejects(getErrorResponse({ errors: ['uh oh!'] }, 400));
|
|
await this.renderComponent();
|
|
await click(GENERAL.submitButton);
|
|
assert.dom(GENERAL.messageError).hasText('Error Authentication failed: uh oh!');
|
|
tokenLookUpSelfStub.restore();
|
|
});
|
|
|
|
test('dropdown does not include enterprise methods on community versions', async function (assert) {
|
|
this.version.type = 'community';
|
|
const supported = supportedTypes(false);
|
|
const unsupported = ENTERPRISE_LOGIN_METHODS.map((m) => m.type);
|
|
assert.expect(supported.length + unsupported.length);
|
|
await this.renderComponent();
|
|
const dropdownOptions = findAll(`${GENERAL.selectByAttr('auth type')} option`).map((o) => o.value);
|
|
|
|
supported.forEach((m) => {
|
|
assert.true(dropdownOptions.includes(m), `dropdown includes supported method: ${m}`);
|
|
});
|
|
unsupported.forEach((m) => {
|
|
assert.false(dropdownOptions.includes(m), `dropdown does NOT include unsupported method: ${m}`);
|
|
});
|
|
});
|
|
|
|
module('listing visibility', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
const defaultTabs = {
|
|
userpass: [
|
|
{
|
|
path: 'userpass/',
|
|
description: '',
|
|
options: {},
|
|
type: 'userpass',
|
|
},
|
|
{
|
|
path: 'userpass2/',
|
|
description: '',
|
|
options: {},
|
|
type: 'userpass',
|
|
},
|
|
],
|
|
ldap: [
|
|
{
|
|
path: 'my-ldap/',
|
|
description: '',
|
|
options: {},
|
|
type: 'ldap',
|
|
},
|
|
],
|
|
token: [
|
|
{
|
|
path: 'token/',
|
|
description: 'token based credentials',
|
|
options: null,
|
|
type: 'token',
|
|
},
|
|
],
|
|
};
|
|
// all computed by the parent, in this case the initial tabs are the same as visible mount types
|
|
// but that isn't always the case
|
|
this.visibleMountTypes = Object.keys(defaultTabs);
|
|
this.defaultView = { type: 'tabs', tabData: defaultTabs };
|
|
this.alternateView = { type: 'dropdown', tabData: null };
|
|
this.initialFormState = { initialAuthType: 'userpass', showAlternate: false };
|
|
});
|
|
|
|
test('it selects each auth tab and renders form for that type', async function (assert) {
|
|
await this.renderComponent();
|
|
const assertSelected = (type) => {
|
|
assert.dom(AUTH_FORM.authForm(type)).exists(`${type}: form renders when tab is selected`);
|
|
assert.dom(AUTH_FORM.tabBtn(type)).hasAttribute('aria-selected', 'true');
|
|
};
|
|
const assertUnselected = (type) => {
|
|
assert.dom(AUTH_FORM.authForm(type)).doesNotExist(`${type}: form does NOT render`);
|
|
assert.dom(AUTH_FORM.tabBtn(type)).hasAttribute('aria-selected', 'false');
|
|
};
|
|
// click through each tab
|
|
await click(AUTH_FORM.tabBtn('userpass'));
|
|
assertSelected('userpass');
|
|
assertUnselected('ldap');
|
|
assertUnselected('token');
|
|
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
|
|
|
|
await click(AUTH_FORM.tabBtn('ldap'));
|
|
assertSelected('ldap');
|
|
assertUnselected('token');
|
|
assertUnselected('userpass');
|
|
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
|
|
|
|
await click(AUTH_FORM.tabBtn('token'));
|
|
assertSelected('token');
|
|
assertUnselected('ldap');
|
|
assertUnselected('userpass');
|
|
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
|
|
});
|
|
|
|
test('it clicks "Sign in with other methods" and toggles to other view', async function (assert) {
|
|
await this.renderComponent();
|
|
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'tabs render by default');
|
|
assert.dom(GENERAL.backButton).doesNotExist();
|
|
await click(GENERAL.button('Sign in with other methods'));
|
|
assert
|
|
.dom(GENERAL.button('Sign in with other methods'))
|
|
.doesNotExist('"Sign in with other methods" does not render after it is clicked');
|
|
assert
|
|
.dom(GENERAL.selectByAttr('auth type'))
|
|
.exists('clicking "Sign in with other methods" renders dropdown instead of tabs');
|
|
await click(GENERAL.backButton);
|
|
assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render after it is clicked');
|
|
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'clicking "Back" renders tabs again');
|
|
assert
|
|
.dom(GENERAL.button('Sign in with other methods'))
|
|
.exists('"Sign in with other methods" renders again');
|
|
});
|
|
|
|
test('it resets selected tab after clicking "Sign in with other methods" and then "Back"', async function (assert) {
|
|
await this.renderComponent();
|
|
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
|
|
assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'false');
|
|
assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'false');
|
|
|
|
// select a different tab before clicking "Sign in with other methods"
|
|
await click(AUTH_FORM.tabBtn('ldap'));
|
|
assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'true');
|
|
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'false');
|
|
await click(GENERAL.button('Sign in with other methods'));
|
|
assert.dom(GENERAL.selectByAttr('auth type')).exists('it renders dropdown instead of tabs');
|
|
await click(GENERAL.backButton);
|
|
// assert tab selection is reset
|
|
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
|
|
assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'false');
|
|
assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'false');
|
|
});
|
|
|
|
test('it preselects tab from initialFormState', async function (assert) {
|
|
this.initialFormState = { initialAuthType: 'ldap', showAlternate: false };
|
|
await this.renderComponent();
|
|
assert.dom(AUTH_FORM.authForm('ldap')).exists('ldap form renders');
|
|
assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'true');
|
|
});
|
|
|
|
test('it renders dropdown and preselects type if initialFormState is not a tab', async function (assert) {
|
|
this.initialFormState = { initialAuthType: 'okta', showAlternate: true };
|
|
await this.renderComponent();
|
|
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('okta');
|
|
assert.dom(GENERAL.inputByAttr('username')).exists();
|
|
assert.dom(GENERAL.inputByAttr('password')).exists();
|
|
|
|
assert.dom(GENERAL.backButton).exists('"Back" button renders');
|
|
assert
|
|
.dom(GENERAL.button('Sign in with other methods'))
|
|
.doesNotExist('"Sign in with other methods" does not render');
|
|
});
|
|
});
|
|
|
|
// tests with "enterprise" in the title are filtered out from CE test runs
|
|
// naming the module 'ent' so these tests still run on the CE repo
|
|
module('ent', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
this.version.type = 'enterprise';
|
|
this.namespaceQueryParam = '';
|
|
});
|
|
|
|
// in the ent module to test ALL supported login methods
|
|
// iterating in tests should generally be avoided, but purposefully wanted to test the component
|
|
// renders as expected as auth types change
|
|
test('it selects each supported auth type and renders its form and relevant fields', async function (assert) {
|
|
const authMethodTypes = supportedTypes(true);
|
|
const totalFields = Object.values(AUTH_METHOD_LOGIN_DATA).reduce(
|
|
(sum, obj) => sum + Object.keys(obj).length,
|
|
0
|
|
);
|
|
// 3 assertions per method, plus an assertion for each expected field
|
|
assert.expect(3 * authMethodTypes.length + totalFields); // count at time of writing is 40
|
|
|
|
await this.renderComponent();
|
|
for (const authType of authMethodTypes) {
|
|
let stub;
|
|
if (['oidc', 'jwt'].includes(authType)) {
|
|
stub = sinon.stub(this.router, 'urlFor').returns('123-example.com');
|
|
}
|
|
const loginData = AUTH_METHOD_LOGIN_DATA[authType];
|
|
|
|
const fields = Object.keys(loginData);
|
|
await fillIn(GENERAL.selectByAttr('auth type'), authType);
|
|
|
|
assert.dom(GENERAL.selectByAttr('auth type')).hasValue(authType), `${authType}: it selects type`;
|
|
assert.dom(AUTH_FORM.authForm(authType)).exists(`${authType}: it renders form component`);
|
|
|
|
// token is the only method that does not support a custom mount path
|
|
if (authType !== 'token') {
|
|
// jwt and oidc render the same component so the toggle remains open switching between those types
|
|
const element = find(AUTH_FORM.advancedSettings);
|
|
if (element.ariaExpanded === 'false') {
|
|
await click(AUTH_FORM.advancedSettings);
|
|
}
|
|
}
|
|
|
|
const assertion = authType === 'token' ? 'doesNotExist' : 'exists';
|
|
assert.dom(GENERAL.inputByAttr('path'))[assertion](`${authType}: mount path input ${assertion}`);
|
|
|
|
fields.forEach((field) => {
|
|
assert.dom(GENERAL.inputByAttr(field)).exists(`${authType}: ${field} input renders`);
|
|
});
|
|
|
|
if (stub) {
|
|
stub.restore();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('dropdown includes enterprise methods', async function (assert) {
|
|
const supported = supportedTypes(true);
|
|
assert.expect(supported.length);
|
|
await this.renderComponent();
|
|
|
|
const dropdownOptions = findAll(`${GENERAL.selectByAttr('auth type')} option`).map((o) => o.value);
|
|
supported.forEach((m) => {
|
|
assert.true(dropdownOptions.includes(m), `dropdown includes supported method: ${m}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
// AUTH METHOD SPECIFIC TESTS
|
|
// since the template yields each auth <form> some assertions are best done here instead of
|
|
// in the corresponding the Auth::Form::<Type> integration tests
|
|
module('oidc-jwt', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
this.routerStub = (path) => sinon.stub(this.router, 'urlFor').returns(`/auth/${path}/oidc/callback`);
|
|
});
|
|
|
|
test('it re-requests the auth_url when authType changes', async function (assert) {
|
|
this.routerStub('oidc');
|
|
assert.expect(2); // auth_url should be hit twice, one for each type selection
|
|
let expectedType = 'oidc';
|
|
this.server.post(`/auth/:path/oidc/auth_url`, (_, req) => {
|
|
assert.strictEqual(
|
|
req.params.path,
|
|
expectedType,
|
|
`it makes request to auth_url for selected type: ${expectedType}`
|
|
);
|
|
return { data: { auth_url: '123-example.com' } };
|
|
});
|
|
await this.renderComponent();
|
|
// auth_url should be requested once when "oidc" is selected
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
|
|
// auth_url should be requested again when "jwt" is selected
|
|
expectedType = 'jwt';
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'jwt');
|
|
});
|
|
|
|
// for simplicity the auth types are configured as their namesake but type isn't relevant.
|
|
// these tests assert that CONFIG changes from OIDC -> JWT render correctly and vice versa
|
|
// so the order the requests are hit is what matters.
|
|
test('"OIDC" to "JWT" configuration: it updates the form when the auth_url response changes', async function (assert) {
|
|
this.routerStub('oidc');
|
|
this.server.post(`/auth/oidc/oidc/auth_url`, () => ({ data: { auth_url: '123-example.com' } })); // this return means mount is configured as oidc
|
|
this.server.post(`/auth/jwt/oidc/auth_url`, () => overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })); // this return means the mount is configured as jwt
|
|
await this.renderComponent();
|
|
|
|
// select mount configured for OIDC first
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
|
|
assert.dom(GENERAL.inputByAttr('jwt')).doesNotExist();
|
|
// then select mount configured for JWT
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'jwt');
|
|
assert.dom(GENERAL.inputByAttr('jwt')).exists();
|
|
});
|
|
|
|
test('"JWT" to "OIDC" configuration: it updates the form when the auth_url response changes', async function (assert) {
|
|
this.routerStub('oidc');
|
|
this.server.post(`/auth/jwt/oidc/auth_url`, () => overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })); // this return means the mount is configured as jwt
|
|
this.server.post(`/auth/oidc/oidc/auth_url`, () => ({ data: { auth_url: '123-example.com' } })); // this return means mount is configured as oidc
|
|
await this.renderComponent();
|
|
|
|
// select mount configured for JWT first
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'jwt');
|
|
assert.dom(GENERAL.inputByAttr('jwt')).exists();
|
|
|
|
// then select mount configured for OIDC
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
|
|
assert.dom(GENERAL.inputByAttr('jwt')).doesNotExist();
|
|
});
|
|
|
|
test('it should retain role input value when mount path changes', async function (assert) {
|
|
assert.expect(2);
|
|
this.routerStub('foo-oidc');
|
|
const auth_url = 'http://dev-foo-bar.com';
|
|
this.server.post('/auth/:path/oidc/auth_url', (_, req) => {
|
|
const { role, redirect_uri } = JSON.parse(req.requestBody);
|
|
const goodRequest =
|
|
req.params.path === 'foo-oidc' &&
|
|
role === 'foo' &&
|
|
redirect_uri.includes('/auth/foo-oidc/oidc/callback');
|
|
if (goodRequest) {
|
|
return { data: { auth_url } };
|
|
} else {
|
|
return overrideResponse(400, { errors: [ERROR_JWT_LOGIN] });
|
|
}
|
|
});
|
|
|
|
window.open = (url) => {
|
|
assert.strictEqual(url, auth_url, 'auth_url is returned when required params are passed');
|
|
};
|
|
|
|
await this.renderComponent();
|
|
|
|
await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
|
|
await fillIn(GENERAL.inputByAttr('role'), 'foo');
|
|
await click(AUTH_FORM.advancedSettings);
|
|
await fillIn(GENERAL.inputByAttr('role'), 'foo');
|
|
await fillIn(GENERAL.inputByAttr('path'), 'foo-oidc');
|
|
assert.dom(GENERAL.inputByAttr('role')).hasValue('foo', 'role is retained when mount path is changed');
|
|
await click(GENERAL.submitButton);
|
|
});
|
|
});
|
|
});
|