mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-26 00:51:08 +02:00
* UI: Move `wrapped_token` login functionality to route (#30465) * move token unwrap functionality to page component * update mfa test * remove wrapped_token logic from page component * more cleanup to relocate unwrap logic * move wrapped_token to route * move unwrap tests to acceptance * move mfa form back * add some padding * update mfa-form tests * get param from params * wait for auth form on back * run rests * UI: Add MFA support for SSO methods (#30489) * initial implementation of mfa validation for sso methods * update typescript interfaces * add stopgap changes to auth service * switch order backend is defined * update login form for tests even though it will be deleted * attempt to stabilize wrapped_query test * =update login form test why not * Update ui/app/components/auth/form/saml.ts Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com> --------- Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com> * Move CSP error to page component (#30492) * initial implementation of mfa validation for sso methods * update typescript interfaces * add stopgap changes to auth service * switch order backend is defined * update login form for tests even though it will be deleted * attempt to stabilize wrapped_query test * =update login form test why not * move csp error to page component * move csp error to page component * Move fetching unauthenticated mounts to the route (#30509) * rename namespace arg to namespaceQueryParam * move fetch mounts to route * add margin to sign in button spacing * update selectors for oidc provider test * add todo delete comments * fix arg typo in test * change method name * fix args handling tab click * remove tests that no longer relate to components functionality * add tests for preselectedAuthType functionality * move typescript interfaces, fix selector * add await * oops * move format method down, make private * move tab formatting to the route * move to page object * fix token unwrap aborting transition * not sure what that is doing there.. * add comments * rename to presetAuthType * use did-insert instead * UI: Implement `Auth::FormTemplate` (#30521) * replace Auth::LoginForm with Auth::FormTemplate * first round of test updates * return null if mounts object is empty * add comment and test for empty sys/internal/mounts data * more test updates * delete listing_visibility test, delete login-form component test * update divs to Hds::Card::Container * add overflow class * remove unused getters * move requesting stored auth type to page component * fix typo * Update ui/app/components/auth/form/oidc-jwt.ts make comment make more sense * small cleanup items, update imports * Delete old auth components (#30527) * delete old components * update codeowners * Update `with` query param functionality (#30537) * update path input to type=hidden * add test coverage * update page test * update auth route * delete login form * update ent test * consolidate logic in getter * add more comments * more comments.. * rename selector * refresh model as well * redirect for invalid query params * move unwrap to redirect * only redirect on invalid query params * add tests for query param * test selector updates * remove todos, update relevant ones with initials * add changelog --------- Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
290 lines
10 KiB
JavaScript
290 lines
10 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { setupRenderingTest } from 'ember-qunit';
|
|
import hbs from 'htmlbars-inline-precompile';
|
|
import { click, fillIn, find, render } from '@ember/test-helpers';
|
|
import sinon from 'sinon';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
|
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
|
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
|
import { windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
|
|
|
module('Integration | Component | auth | form | saml', function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
this.authType = 'saml';
|
|
this.expectedFields = ['role'];
|
|
|
|
this.authenticateStub = sinon.stub(this.owner.lookup('service:auth'), 'authenticate');
|
|
this.store = this.owner.lookup('service:store');
|
|
this.cluster = { id: 1 };
|
|
this.onError = sinon.spy();
|
|
this.onSuccess = sinon.spy();
|
|
this.windowStub = windowStub();
|
|
sinon.replaceGetter(window, 'screen', () => ({ height: 600, width: 500 }));
|
|
|
|
// role request
|
|
this.server.put('/auth/saml/sso_service_url', () => {
|
|
return {
|
|
data: {
|
|
sso_service_url: 'https://my-single-sign-on-url.com',
|
|
token_poll_id: '4fe2ec01-1f56-b665-0ba2-09c7bca10ae8',
|
|
},
|
|
};
|
|
});
|
|
// polling request
|
|
this.server.put('/auth/saml/token', () => {
|
|
return { auth: { client_token: 'my_token' } };
|
|
});
|
|
|
|
this.renderComponent = ({ yieldBlock = false } = {}) => {
|
|
if (yieldBlock) {
|
|
return render(hbs`
|
|
<Auth::Form::Saml
|
|
@authType={{this.authType}}
|
|
@cluster={{this.cluster}}
|
|
@onError={{this.onError}}
|
|
@onSuccess={{this.onSuccess}}
|
|
>
|
|
<:advancedSettings>
|
|
<label for="path">Mount path</label>
|
|
<input data-test-input="path" id="path" name="path" type="text" />
|
|
</:advancedSettings>
|
|
</Auth::Form::Saml>`);
|
|
}
|
|
return render(hbs`
|
|
<Auth::Form::Saml
|
|
@authType={{this.authType}}
|
|
@cluster={{this.cluster}}
|
|
@onError={{this.onError}}
|
|
@onSuccess={{this.onSuccess}}
|
|
/>`);
|
|
};
|
|
});
|
|
|
|
hooks.afterEach(function () {
|
|
this.windowStub.restore();
|
|
sinon.restore();
|
|
});
|
|
|
|
test('it renders helper text', async function (assert) {
|
|
await this.renderComponent();
|
|
const id = find(GENERAL.inputByAttr('role')).id;
|
|
assert
|
|
.dom(`#helper-text-${id}`)
|
|
.hasText('Vault will use the default role to sign in if this field is left blank.');
|
|
});
|
|
|
|
test('it renders warning if insecure context is detected', async function (assert) {
|
|
sinon.replaceGetter(window, 'isSecureContext', () => false);
|
|
|
|
await this.renderComponent();
|
|
assert
|
|
.dom('[data-test-saml-auth-not-allowed]')
|
|
.hasText(
|
|
'Insecure context detected Logging in with a SAML auth method requires a browser in a secure context. Read more about secure contexts.'
|
|
);
|
|
});
|
|
|
|
test('it requests sso_service_url and opens popup on submit if role is empty', async function (assert) {
|
|
assert.expect(6);
|
|
this.server.put('/auth/saml/sso_service_url', (_, req) => {
|
|
const { acs_url, role } = JSON.parse(req.requestBody);
|
|
assert.strictEqual(acs_url, `${window.origin}/v1/auth/saml/callback`, 'it builds acs_url for payload');
|
|
assert.strictEqual(role, '', 'role has no value');
|
|
return {
|
|
data: {
|
|
sso_service_url: 'https://my-single-sign-on-url.com',
|
|
token_poll_id: '4fe2ec01-1f56-b665-0ba2-09c7bca10ae8',
|
|
},
|
|
};
|
|
});
|
|
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
|
|
const [sso_service_url, name, dimensions] = this.windowStub.lastCall.args;
|
|
assert.strictEqual(
|
|
sso_service_url,
|
|
'https://my-single-sign-on-url.com',
|
|
'it calls window opener with sso_service_url returned by role request'
|
|
);
|
|
assert.strictEqual(sso_service_url, 'https://my-single-sign-on-url.com');
|
|
assert.strictEqual(name, 'vaultSAMLWindow', 'it calls window opener with expected name');
|
|
assert.strictEqual(
|
|
dimensions,
|
|
'width=500,height=600,resizable,scrollbars=yes,top=0,left=0',
|
|
'it calls window opener with expected dimensions'
|
|
);
|
|
});
|
|
|
|
test('it requests sso_service_url with inputted role and default path', async function (assert) {
|
|
assert.expect(6);
|
|
this.server.put('/auth/saml/sso_service_url', (_, req) => {
|
|
const { acs_url, role } = JSON.parse(req.requestBody);
|
|
assert.strictEqual(acs_url, `${window.origin}/v1/auth/saml/callback`, 'it builds acs_url for payload');
|
|
assert.strictEqual(role, 'some-dev', 'payload contains role');
|
|
return {
|
|
data: {
|
|
sso_service_url: 'https://my-single-sign-on-url.com',
|
|
token_poll_id: '4fe2ec01-1f56-b665-0ba2-09c7bca10ae8',
|
|
},
|
|
};
|
|
});
|
|
|
|
await this.renderComponent();
|
|
await fillIn(GENERAL.inputByAttr('role'), 'some-dev');
|
|
await click(AUTH_FORM.login);
|
|
|
|
const [sso_service_url, name, dimensions] = this.windowStub.lastCall.args;
|
|
assert.strictEqual(
|
|
sso_service_url,
|
|
'https://my-single-sign-on-url.com',
|
|
'it calls window opener with sso_service_url returned by role request'
|
|
);
|
|
assert.strictEqual(sso_service_url, 'https://my-single-sign-on-url.com');
|
|
assert.strictEqual(name, 'vaultSAMLWindow', 'it calls window opener with expected name');
|
|
assert.strictEqual(
|
|
dimensions,
|
|
'width=500,height=600,resizable,scrollbars=yes,top=0,left=0',
|
|
'it calls window opener with expected dimensions'
|
|
);
|
|
});
|
|
|
|
test('it requests sso_service_url with custom path', async function (assert) {
|
|
assert.expect(6);
|
|
const path = 'custom-path';
|
|
this.server.put(`/auth/${path}/sso_service_url`, (_, req) => {
|
|
const { acs_url, role } = JSON.parse(req.requestBody);
|
|
assert.strictEqual(
|
|
acs_url,
|
|
`${window.origin}/v1/auth/${path}/callback`,
|
|
'it builds acs_url for payload'
|
|
);
|
|
assert.strictEqual(role, 'some-dev', 'payload contains role');
|
|
return {
|
|
data: {
|
|
sso_service_url: 'https://my-single-sign-on-url.com',
|
|
token_poll_id: '4fe2ec01-1f56-b665-0ba2-09c7bca10ae8',
|
|
},
|
|
};
|
|
});
|
|
|
|
await this.renderComponent({ yieldBlock: true });
|
|
await fillIn(GENERAL.inputByAttr('role'), 'some-dev');
|
|
await fillIn(GENERAL.inputByAttr('path'), path);
|
|
await click(AUTH_FORM.login);
|
|
|
|
const [sso_service_url, name, dimensions] = this.windowStub.lastCall.args;
|
|
assert.strictEqual(
|
|
sso_service_url,
|
|
'https://my-single-sign-on-url.com',
|
|
'it calls window opener with sso_service_url returned by role request'
|
|
);
|
|
assert.strictEqual(sso_service_url, 'https://my-single-sign-on-url.com');
|
|
assert.strictEqual(name, 'vaultSAMLWindow', 'it calls window opener with expected name');
|
|
assert.strictEqual(
|
|
dimensions,
|
|
'width=500,height=600,resizable,scrollbars=yes,top=0,left=0',
|
|
'it calls window opener with expected dimensions'
|
|
);
|
|
});
|
|
|
|
test('it polls token request', async function (assert) {
|
|
assert.expect(2); // auth/saml/token url should be requested twice
|
|
|
|
let count = 0;
|
|
this.server.put('/auth/saml/token', () => {
|
|
count++;
|
|
const msg =
|
|
count === 1
|
|
? 'it makes initial request to token url'
|
|
: 'it re-requests token url if httpStatus was 401';
|
|
assert.true(true, msg);
|
|
|
|
if (count === 1) {
|
|
return overrideResponse(401);
|
|
} else {
|
|
return { auth: { client_token: 'my_token' } };
|
|
}
|
|
});
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
});
|
|
|
|
test('it calls auth service with token request callback data', async function (assert) {
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
|
|
const [actual] = this.authenticateStub.lastCall.args;
|
|
assert.propEqual(
|
|
actual.data,
|
|
{
|
|
token: 'my_token',
|
|
},
|
|
'auth service "authenticate" method is called token callback data'
|
|
);
|
|
});
|
|
|
|
test('it calls onSuccess if auth service authentication is successful', async function (assert) {
|
|
const expectedResponse = {
|
|
namespace: '',
|
|
token: 'my_token',
|
|
isRoot: false,
|
|
};
|
|
// stub happy response
|
|
this.authenticateStub.returns(expectedResponse);
|
|
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
const [actualResponse, methodData] = this.onSuccess.lastCall.args;
|
|
assert.propEqual(actualResponse, expectedResponse, 'onSuccess is called with auth response');
|
|
assert.strictEqual(methodData.path, undefined, 'onSuccess is called without path value');
|
|
assert.strictEqual(methodData.selectedAuth, 'saml', 'onSuccess is called with selected auth type');
|
|
});
|
|
|
|
test('it calls onError if auth service authentication fails', async function (assert) {
|
|
this.authenticateStub.throws('permission denied!!');
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
const [actual] = this.onError.lastCall.args;
|
|
assert.strictEqual(
|
|
actual,
|
|
'Authentication failed: Sinon-provided permission denied!!',
|
|
'onError called with auth service failure'
|
|
);
|
|
});
|
|
|
|
test('it calls onError if sso_service_url request fails', async function (assert) {
|
|
// role request
|
|
this.server.put('/auth/saml/sso_service_url', () => overrideResponse(403));
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
const [actual] = this.onError.lastCall.args;
|
|
assert.strictEqual(
|
|
actual,
|
|
'Authentication failed: permission denied',
|
|
'onError called with sso_service_url failure'
|
|
);
|
|
});
|
|
|
|
test('it calls onError if polling token errors in status code that is NOT 401', async function (assert) {
|
|
this.server.put('/auth/saml/token', () => overrideResponse(500));
|
|
await this.renderComponent();
|
|
await click(AUTH_FORM.login);
|
|
const [actual] = this.onError.lastCall.args;
|
|
assert.strictEqual(
|
|
actual,
|
|
'Authentication failed: Ember Data Request PUT /v1/auth/saml/token returned a 500\nPayload (application/json)\n{}',
|
|
'onError called with auth failure'
|
|
);
|
|
});
|
|
});
|