diff --git a/changelog/_22640.txt b/changelog/_22640.txt new file mode 100644 index 0000000000..cbc85c7511 --- /dev/null +++ b/changelog/_22640.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: Add support for SAML login flow +``` diff --git a/ui/app/adapters/auth-method.js b/ui/app/adapters/auth-method.js index c23a1ad469..01d7465174 100644 --- a/ui/app/adapters/auth-method.js +++ b/ui/app/adapters/auth-method.js @@ -68,6 +68,12 @@ export default ApplicationAdapter.extend({ return this.ajax(`/v1/auth/${encodePath(path)}/oidc/callback`, 'GET', { data: { state, code } }); }, + pollSAMLToken(path, token_poll_id, client_verifier) { + return this.ajax(`/v1/auth/${encodePath(path)}/token`, 'PUT', { + data: { token_poll_id, client_verifier }, + }); + }, + tune(path, data) { const url = `${this.buildURL()}/${this.pathForType()}/${encodePath(path)}tune`; return this.ajax(url, 'POST', { data }); diff --git a/ui/app/adapters/role-saml.js b/ui/app/adapters/role-saml.js new file mode 100644 index 0000000000..052f2024f6 --- /dev/null +++ b/ui/app/adapters/role-saml.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import ApplicationAdapter from './application'; +import { inject as service } from '@ember/service'; +import { encodePath } from 'vault/utils/path-encoding-helpers'; +import { v4 as uuidv4 } from 'uuid'; + +export default ApplicationAdapter.extend({ + router: service(), + + // generateClientChallenge generates a client challenge from a verifier. + // The client challenge is the base64(sha256(verifier)). The verifier is + // later presented to the server to obtain the resulting Vault token. + async generateClientChallenge(verifier) { + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = new Uint8Array(hashBuffer); + return btoa(String.fromCharCode.apply(null, hashArray)); + }, + + async findRecord(store, type, id, snapshot) { + let [path, role] = JSON.parse(id); + path = preparePathSegment(path); + + // Create the ACS URL based on the cluster the UI is targeting + let acs_url = `${window.location.origin}/v1/`; + let namespace = snapshot?.adapterOptions.namespace; + if (namespace) { + namespace = preparePathSegment(namespace); + acs_url = acs_url.concat(namespace, '/'); + } + acs_url = acs_url.concat('auth/', path, '/callback'); + + // Create the client verifier and challenge + const verifier = uuidv4(); + const challenge = await this.generateClientChallenge(verifier); + // Kick off the authentication flow by generating the SSO service URL + // It requires the client challenge generated from the verifier. We'll + // later provide the verifier to match up with the challenge on the server + // when we poll for the Vault token by its returned token poll ID. + const response = await this.ajax(`/v1/auth/${path}/sso_service_url`, 'PUT', { + data: { + acs_url, + role, + client_challenge: challenge, + client_type: 'browser', + }, + }); + return { + ...response.data, + client_verifier: verifier, + }; + }, +}); + +// preparePathSegment prepares the given segment for being included in a URL +// path by trimming leading and trailing forward slashes and URL encoding. +function preparePathSegment(segment) { + segment = segment.replace(/^\//, ''); + segment = segment.replace(/\/$/, ''); + return encodePath(segment); +} diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js index e0ca05e7c6..4f44fdc6d3 100644 --- a/ui/app/components/auth-form.js +++ b/ui/app/components/auth-form.js @@ -10,13 +10,11 @@ import { match, alias, or } from '@ember/object/computed'; import { dasherize } from '@ember/string'; import Component from '@ember/component'; import { computed } from '@ember/object'; -import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; +import { allSupportedAuthBackends, supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; import { task, timeout } from 'ember-concurrency'; import { waitFor } from '@ember/test-waiters'; import { v4 as uuidv4 } from 'uuid'; -const BACKENDS = supportedAuthBackends(); - /** * @module AuthForm * The `AuthForm` is used to sign users into Vault. @@ -49,6 +47,7 @@ export default Component.extend(DEFAULTS, { flashMessages: service(), store: service(), csp: service('csp-event'), + version: service(), // passed in via a query param selectedAuth: null, @@ -58,11 +57,14 @@ export default Component.extend(DEFAULTS, { wrappedToken: null, // internal oldNamespace: null, - authMethods: BACKENDS, // number answer for okta number challenge if applicable oktaNumberChallengeAnswer: null, + authMethods: computed('version.isEnterprise', function () { + return this.version.isEnterprise ? allSupportedAuthBackends() : supportedAuthBackends(); + }), + didReceiveAttrs() { this._super(...arguments); const { @@ -139,7 +141,7 @@ export default Component.extend(DEFAULTS, { if (keyIsPath && !type) { return methods.findBy('path', selected); } - return BACKENDS.findBy('type', selected); + return this.authMethods.findBy('type', selected); }, selectedAuthIsPath: match('selectedAuth', /\/$/), @@ -168,21 +170,21 @@ export default Component.extend(DEFAULTS, { cspErrorText: `This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`, - allSupportedMethods: computed('methodsToShow', 'hasMethodsWithPath', function () { + allSupportedMethods: computed('methodsToShow', 'hasMethodsWithPath', 'authMethods', function () { const hasMethodsWithPath = this.hasMethodsWithPath; const methodsToShow = this.methodsToShow; - return hasMethodsWithPath ? methodsToShow.concat(BACKENDS) : methodsToShow; + return hasMethodsWithPath ? methodsToShow.concat(this.authMethods) : methodsToShow; }), hasMethodsWithPath: computed('methodsToShow', function () { return this.methodsToShow.isAny('path'); }), - methodsToShow: computed('methods', function () { + methodsToShow: computed('methods', 'authMethods', function () { const methods = this.methods || []; const shownMethods = methods.filter((m) => - BACKENDS.find((b) => b.type.toLowerCase() === m.type.toLowerCase()) + this.authMethods.find((b) => b.type.toLowerCase() === m.type.toLowerCase()) ); - return shownMethods.length ? shownMethods : BACKENDS; + return shownMethods.length ? shownMethods : this.authMethods; }), unwrapToken: task( @@ -299,9 +301,9 @@ export default Component.extend(DEFAULTS, { this.set('token', token); } this.set('error', null); - // if callback from oidc or jwt we have a token at this point + // if callback from oidc, jwt, or saml we have a token at this point const backend = token ? this.getAuthBackend('token') : this.selectedAuthBackend || {}; - const backendMeta = BACKENDS.find( + const backendMeta = this.authMethods.find( (b) => (b.type || '').toLowerCase() === (backend.type || '').toLowerCase() ); const attributes = (backendMeta || {}).formAttributes || []; diff --git a/ui/app/components/auth-saml.js b/ui/app/components/auth-saml.js new file mode 100644 index 0000000000..9b5814e800 --- /dev/null +++ b/ui/app/components/auth-saml.js @@ -0,0 +1,162 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { inject as service } from '@ember/service'; +import Component from './outer-html'; +import { task, timeout, waitForEvent } from 'ember-concurrency'; +import { computed } from '@ember/object'; +import errorMessage from 'vault/utils/error-message'; + +const WAIT_TIME = 500; +const ERROR_WINDOW_CLOSED = + 'The provider window was closed before authentication was complete. Your web browser may have blocked or closed a pop-up window. Please check your settings and click Sign In to try again.'; +const ERROR_TIMEOUT = 'The authentication request has timed out. 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.'; +export { ERROR_WINDOW_CLOSED, ERROR_MISSING_PARAMS }; + +export default Component.extend({ + store: service(), + featureFlagService: service('featureFlag'), + + selectedAuthPath: null, + selectedAuthType: null, + roleName: null, + errorMessage: null, + onRoleName() {}, + onLoading() {}, + onError() {}, + onNamespace() {}, + + didReceiveAttrs() { + this._super(); + this.set('errorMessage', null); + }, + + getWindow() { + return this.window || window; + }, + + canLoginSaml: computed('getWindow', function () { + return this.getWindow().isSecureContext; + }), + + async fetchRole(roleName) { + const path = this.selectedAuthPath || this.selectedAuthType; + const id = JSON.stringify([path, roleName]); + return this.store.findRecord('role-saml', id, { + adapterOptions: { namespace: this.namespace }, + }); + }, + + cancelLogin(samlWindow, errorMessage) { + this.closeWindow(samlWindow); + this.handleSAMLError(errorMessage); + this.exchangeSAMLTokenPollID.cancelAll(); + }, + + closeWindow(samlWindow) { + this.watchPopup.cancelAll(); + this.watchCurrent.cancelAll(); + samlWindow.close(); + }, + + handleSAMLError(err) { + this.onLoading(false); + this.onError(err); + }, + + watchPopup: task(function* (samlWindow) { + while (true) { + yield timeout(WAIT_TIME); + if (!samlWindow || samlWindow.closed) { + this.exchangeSAMLTokenPollID.cancelAll(); + return this.handleSAMLError(ERROR_WINDOW_CLOSED); + } + } + }), + + watchCurrent: task(function* (samlWindow) { + // when user is about to change pages, close the popup window + yield waitForEvent(this.getWindow(), 'beforeunload'); + samlWindow?.close(); + }), + + exchangeSAMLTokenPollID: task(function* (samlWindow, role) { + this.onLoading(true); + + // start watching the popup window and the current one + this.watchPopup.perform(samlWindow); + this.watchCurrent.perform(samlWindow); + + const path = this.selectedAuthPath || this.selectedAuthType; + const adapter = this.store.adapterFor('auth-method'); + this.onNamespace(this.namespace); + + // Wait up to 3 minutes for the token to become available + let resp; + for (let i = 0; i < 180; i++) { + yield timeout(WAIT_TIME); + try { + resp = yield adapter.pollSAMLToken(path, role.tokenPollID, role.clientVerifier); + if (!resp?.auth) { + continue; + } + // We've obtained the Vault token for the authentication flow, now log in. + yield this.onSubmit(null, null, resp.auth.client_token); + this.closeWindow(samlWindow); + return; + } catch (e) { + if (e.httpStatus === 401) { + // Continue to retry on 401 Unauthorized + continue; + } + return this.cancelLogin(samlWindow, errorMessage(e)); + } + } + this.cancelLogin(samlWindow, ERROR_TIMEOUT); + }), + + actions: { + setRole(roleName) { + this.onRoleName(roleName); + }, + /* Saml auth flow on login button click: + * 1. find role-saml record which returns role info + * 2. open popup at url defined returned from role + * 3. watch popup window for close (and cancel polling if it closes) + * 4. poll vault for 200 token response + * 5. close popup, stop polling, and trigger onSubmit with token data + */ + async startSAMLAuth(callback, data, e) { + this.onError(null); + this.onLoading(true); + if (e && e.preventDefault) { + e.preventDefault(); + } + const roleName = data.role; + let role; + try { + role = await this.fetchRole(roleName); + } catch (error) { + this.handleSAMLError(error); + return; + } + + const win = this.getWindow(); + const POPUP_WIDTH = 500; + const POPUP_HEIGHT = 600; + const left = win.screen.width / 2 - POPUP_WIDTH / 2; + const top = win.screen.height / 2 - POPUP_HEIGHT / 2; + const samlWindow = win.open( + role.ssoServiceURL, + 'vaultSAMLWindow', + `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},resizable,scrollbars=yes,top=${top},left=${left}` + ); + + this.exchangeSAMLTokenPollID.perform(samlWindow, role); + }, + }, +}); diff --git a/ui/app/components/mount-backend/type-form.js b/ui/app/components/mount-backend/type-form.js index cdec52a095..f6cf98fda6 100644 --- a/ui/app/components/mount-backend/type-form.js +++ b/ui/app/components/mount-backend/type-form.js @@ -5,7 +5,7 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; -import { methods } from 'vault/helpers/mountable-auth-methods'; +import { allMethods, methods } from 'vault/helpers/mountable-auth-methods'; import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; import { tracked } from '@glimmer/tracking'; @@ -31,7 +31,11 @@ export default class MountBackendTypeForm extends Component { return this.version.isEnterprise ? allEngines() : mountableEngines(); } + get authMethods() { + return this.version.isEnterprise ? allMethods() : methods(); + } + get mountTypes() { - return this.args.mountType === 'secret' ? this.secretEngines : methods(); + return this.args.mountType === 'secret' ? this.secretEngines : this.authMethods; } } diff --git a/ui/app/helpers/mountable-auth-methods.js b/ui/app/helpers/mountable-auth-methods.js index bdb865f8a1..303e9baff4 100644 --- a/ui/app/helpers/mountable-auth-methods.js +++ b/ui/app/helpers/mountable-auth-methods.js @@ -5,6 +5,15 @@ import { helper as buildHelper } from '@ember/component/helper'; +const ENTERPRISE_AUTH_METHODS = [ + { + displayName: 'SAML', + value: 'saml', + type: 'saml', + category: 'generic', + }, +]; + const MOUNTABLE_AUTH_METHODS = [ { displayName: 'AliCloud', @@ -106,4 +115,8 @@ export function methods() { return MOUNTABLE_AUTH_METHODS.slice(); } +export function allMethods() { + return [...MOUNTABLE_AUTH_METHODS, ...ENTERPRISE_AUTH_METHODS]; +} + export default buildHelper(methods); diff --git a/ui/app/helpers/supported-auth-backends.js b/ui/app/helpers/supported-auth-backends.js index 10361bc839..e06cbd3387 100644 --- a/ui/app/helpers/supported-auth-backends.js +++ b/ui/app/helpers/supported-auth-backends.js @@ -72,8 +72,23 @@ const SUPPORTED_AUTH_BACKENDS = [ }, ]; +const ENTERPRISE_AUTH_METHODS = [ + { + type: 'saml', + typeDisplay: 'SAML', + description: 'Authenticate using SAML provider.', + tokenPath: 'client_token', + displayNamePath: 'display_name', + formAttributes: ['role'], + }, +]; + export function supportedAuthBackends() { return SUPPORTED_AUTH_BACKENDS; } +export function allSupportedAuthBackends() { + return [...SUPPORTED_AUTH_BACKENDS, ...ENTERPRISE_AUTH_METHODS]; +} + export default buildHelper(supportedAuthBackends); diff --git a/ui/app/models/role-saml.js b/ui/app/models/role-saml.js new file mode 100644 index 0000000000..430845b094 --- /dev/null +++ b/ui/app/models/role-saml.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Model, { attr } from '@ember-data/model'; + +export default class RoleSamlModel extends Model { + @attr('string') ssoServiceURL; + @attr('string') tokenPollID; + @attr('string') clientVerifier; +} diff --git a/ui/app/services/auth.js b/ui/app/services/auth.js index a66ccfdc1f..d44b41bfb7 100644 --- a/ui/app/services/auth.js +++ b/ui/app/services/auth.js @@ -17,12 +17,12 @@ import { resolve, reject } from 'rsvp'; import getStorage from 'vault/lib/token-storage'; import ENV from 'vault/config/environment'; -import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; +import { allSupportedAuthBackends } from 'vault/helpers/supported-auth-backends'; const TOKEN_SEPARATOR = '☃'; const TOKEN_PREFIX = 'vault-'; const ROOT_PREFIX = '_root_'; -const BACKENDS = supportedAuthBackends(); +const BACKENDS = allSupportedAuthBackends(); export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX }; diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs index f93b524dc2..cb56335804 100644 --- a/ui/app/templates/components/auth-form.hbs +++ b/ui/app/templates/components/auth-form.hbs @@ -52,12 +52,14 @@ {{/if}}
-
-

{{this.selectedAuthBackend.path}}

- - {{this.selectedAuthBackend.mountDescription}} - -
+ {{#if this.selectedAuthBackend.path}} +
+

{{this.selectedAuthBackend.path}}

+ + {{this.selectedAuthBackend.mountDescription}} + +
+ {{/if}} {{#if (or (not this.hasMethodsWithPath) (not this.selectedAuthIsPath))}} +
+ + +
+ {{yield}} +
+ + +{{else}} + + Nonsecure context detected + + Logging in with a SAML auth method requires a browser in a secure context. + + + + Read more about secure contexts. + + + + +{{/if}} \ No newline at end of file diff --git a/ui/tests/acceptance/auth-list-test.js b/ui/tests/acceptance/auth-list-test.js index 2a716e49b6..0ac6e53a8f 100644 --- a/ui/tests/acceptance/auth-list-test.js +++ b/ui/tests/acceptance/auth-list-test.js @@ -11,7 +11,7 @@ import { v4 as uuidv4 } from 'uuid'; import authPage from 'vault/tests/pages/auth'; import enablePage from 'vault/tests/pages/settings/auth/enable'; -import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; +import { allSupportedAuthBackends, supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; import { supportedManagedAuthBackends } from 'vault/helpers/supported-managed-auth-backends'; import { deleteAuthCmd, mountAuthCmd, runCmd } from 'vault/tests/helpers/commands'; @@ -113,4 +113,42 @@ module('Acceptance | auth backend list', function (hooks) { } } }); + + test('enterprise: auth methods are linkable and link to correct view', async function (assert) { + assert.expect(19); + const uid = uuidv4(); + await visit('/vault/access'); + + const supportManaged = supportedManagedAuthBackends(); + const backends = allSupportedAuthBackends(); + for (const backend of backends) { + const { type } = backend; + const path = `auth-list-${type}-${uid}`; + if (type !== 'token') { + await enablePage.enable(type, path); + } + await settled(); + await visit('/vault/access'); + + // all auth methods should be linkable + await click(`[data-test-auth-backend-link="${type === 'token' ? type : path}"]`); + if (!supportManaged.includes(type)) { + assert.dom('[data-test-auth-section-tab]').exists({ count: 1 }); + assert + .dom('[data-test-auth-section-tab]') + .hasText('Configuration', `only shows configuration tab for ${type} auth method`); + assert.dom('[data-test-doc-link] .doc-link').exists(`includes doc link for ${type} auth method`); + } else { + let expectedTabs = 2; + if (type == 'ldap' || type === 'okta') { + expectedTabs = 3; + } + assert + .dom('[data-test-auth-section-tab]') + .exists({ count: expectedTabs }, `has management tabs for ${type} auth method`); + // cleanup method + await runCmd(deleteAuthCmd(path)); + } + } + }); }); diff --git a/ui/tests/acceptance/saml-auth-method-test.js b/ui/tests/acceptance/saml-auth-method-test.js new file mode 100644 index 0000000000..c95ff37fee --- /dev/null +++ b/ui/tests/acceptance/saml-auth-method-test.js @@ -0,0 +1,161 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import sinon from 'sinon'; +import { click, fillIn, find, waitUntil } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { Response } from 'miragejs'; +import authPage from 'vault/tests/pages/auth'; +import { fakeWindow } from 'vault/tests/helpers/oidc-window-stub'; + +module('Acceptance | enterprise saml auth method', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.openStub = sinon.stub(window, 'open').callsFake(() => fakeWindow.create()); + this.server.put('/auth/saml/sso_service_url', () => ({ + data: { + sso_service_url: 'http://sso-url.hashicorp.com/service', + token_poll_id: '1234', + }, + })); + this.server.put('/auth/saml/token', () => ({ + auth: { client_token: 'root' }, + })); + // ensure clean state + localStorage.removeItem('selectedAuth'); + authPage.logout(); + }); + + hooks.afterEach(function () { + this.openStub.restore(); + }); + + test('it should login with saml when selected from auth methods dropdown', async function (assert) { + assert.expect(1); + + this.server.get('/auth/token/lookup-self', (schema, req) => { + assert.ok(true, 'request made to auth/token/lookup-self after saml callback'); + req.passthrough(); + }); + // select from dropdown or click auth path tab + await waitUntil(() => find('[data-test-select="auth-method"]')); + await fillIn('[data-test-select="auth-method"]', 'saml'); + await click('[data-test-auth-submit]'); + }); + + test('it should login with saml from listed auth mount tab', async function (assert) { + assert.expect(4); + + this.server.get('/sys/internal/ui/mounts', () => ({ + data: { + auth: { + 'test-path/': { description: '', options: {}, type: 'saml' }, + }, + }, + })); + this.server.put('/auth/test-path/sso_service_url', () => { + assert.ok(true, 'role request made to correct non-standard mount path'); + return { + data: { + sso_service_url: 'http://sso-url.hashicorp.com/service', + token_poll_id: '1234', + }, + }; + }); + this.server.put('/auth/test-path/token', () => { + assert.ok(true, 'login request made to correct non-standard mount path'); + return { + auth: { client_token: 'root' }, + }; + }); + this.server.get('/auth/token/lookup-self', (schema, req) => { + assert.ok(true, 'request made to auth/token/lookup-self after oidc callback'); + assert.deepEqual( + req.requestHeaders, + { 'X-Vault-Token': 'root' }, + 'calls lookup-self with returned client token after login' + ); + req.passthrough(); + }); + + // click auth path tab + await waitUntil(() => find('[data-test-auth-method="test-path"]')); + await click('[data-test-auth-method="test-path"]'); + await click('[data-test-auth-submit]'); + }); + + test('it should render API errors from both endpoints', async function (assert) { + assert.expect(3); + + this.server.put('/auth/saml/sso_service_url', (schema, { requestBody }) => { + const { role } = JSON.parse(requestBody); + if (!role) { + return new Response( + 400, + { 'Content-Type': 'application/json' }, + JSON.stringify({ errors: ["missing required 'role' parameter"] }) + ); + } + return { + data: { + sso_service_url: 'http://sso-url.hashicorp.com/service', + token_poll_id: '1234', + }, + }; + }); + this.server.put('/auth/saml/token', (schema, { requestHeaders }) => { + if (requestHeaders['X-Vault-Namespace']) { + return new Response( + 400, + { 'Content-Type': 'application/json' }, + JSON.stringify({ errors: ['something went wrong'] }) + ); + } + return { + auth: { client_token: 'root' }, + }; + }); + this.server.get('/auth/token/lookup-self', (schema, req) => { + assert.ok(true, 'request made to auth/token/lookup-self after saml callback'); + req.passthrough(); + }); + + // select saml auth type + await waitUntil(() => find('[data-test-select="auth-method"]')); + await fillIn('[data-test-select="auth-method"]', 'saml'); + await fillIn('[data-test-auth-form-ns-input]', 'some-ns'); + await click('[data-test-auth-submit]'); + assert + .dom('[data-test-message-error-description]') + .hasText("missing required 'role' parameter", 'shows API error from role fetch'); + + await fillIn('[data-test-role]', 'my-role'); + await click('[data-test-auth-submit]'); + assert + .dom('[data-test-message-error-description]') + .hasText('something went wrong', 'shows API error from login attempt'); + + await fillIn('[data-test-auth-form-ns-input]', ''); + await click('[data-test-auth-submit]'); + }); + + test('it should populate saml auth method on logout', async function (assert) { + authPage.logout(); + // select from dropdown + await waitUntil(() => find('[data-test-select="auth-method"]')); + await fillIn('[data-test-select="auth-method"]', 'saml'); + await click('[data-test-auth-submit]'); + await waitUntil(() => find('[data-test-user-menu-trigger]')); + await click('[data-test-user-menu-trigger]'); + await click('#logout'); + assert + .dom('[data-test-select="auth-method"]') + .hasValue('saml', 'Previous auth method selected on logout'); + }); +}); diff --git a/ui/tests/integration/components/mount-backend/type-form-test.js b/ui/tests/integration/components/mount-backend/type-form-test.js index 7f89ba8dcc..14c2491941 100644 --- a/ui/tests/integration/components/mount-backend/type-form-test.js +++ b/ui/tests/integration/components/mount-backend/type-form-test.js @@ -9,11 +9,12 @@ import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; -import { methods } from 'vault/helpers/mountable-auth-methods'; +import { allMethods, methods } from 'vault/helpers/mountable-auth-methods'; const secretTypes = mountableEngines().map((engine) => engine.type); const allSecretTypes = allEngines().map((engine) => engine.type); const authTypes = methods().map((auth) => auth.type); +const allAuthTypes = allMethods().map((auth) => auth.type); module('Integration | Component | mount-backend/type-form', function (hooks) { setupRenderingTest(hooks); @@ -70,5 +71,12 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { .dom('[data-test-mount-type]') .exists({ count: allSecretTypes.length }, 'Renders all secret engines'); }); + + test('it renders correct items for enterprise auth methods', async function (assert) { + await render(hbs``); + assert + .dom('[data-test-mount-type]') + .exists({ count: allAuthTypes.length }, 'Renders all secret engines'); + }); }); });