mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-04 20:06:27 +02:00
UI: Refactor auth controller so it does less (#27710)
* move some auth controller logic to route page component * remove unused vars * fix action handling so this context is retained * rename authpage to auth-form-page * rename auth-route-page to auth-splash-page * link jira VAULT-28251 * wowww typo * add padding to mfa form alert message * update component name in tests * alphabetize args * use auth helpers for login method * remove async, await * rename components * update jsdoc * add comment
This commit is contained in:
parent
d9cd3a094a
commit
d35a915f57
23
ui/app/components/auth/login-form.hbs
Normal file
23
ui/app/components/auth/login-form.hbs
Normal file
@ -0,0 +1,23 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#if this.waitingForOktaNumberChallenge}}
|
||||
<OktaNumberChallenge
|
||||
@correctAnswer={{this.oktaNumberChallengeAnswer}}
|
||||
@hasError={{this.authError}}
|
||||
@onReturnToLogin={{this.onCancel}}
|
||||
/>
|
||||
{{else}}
|
||||
<AuthForm
|
||||
@wrappedToken={{@wrappedToken}}
|
||||
@cluster={{@cluster}}
|
||||
@namespace={{@namespace}}
|
||||
@selectedAuth={{@selectedAuth}}
|
||||
@error={{this.authError}}
|
||||
@performAuth={{this.performAuth}}
|
||||
@authIsRunning={{this.authenticate.isRunning}}
|
||||
@delayIsIdle={{this.delayAuthMessageReminder.isIdle}}
|
||||
/>
|
||||
{{/if}}
|
||||
108
ui/app/components/auth/login-form.js
Normal file
108
ui/app/components/auth/login-form.js
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import Ember from 'ember';
|
||||
import { service } from '@ember/service';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* @module AuthLoginForm
|
||||
* The Auth::LoginForm wraps OktaNumberChallenge and AuthForm to manage the login flow and is responsible for calling the authenticate method
|
||||
*
|
||||
* @example
|
||||
* <Auth::LoginForm @wrappedToken={{this.wrappedToken}} @cluster={{this.model}} @namespace={{this.namespaceQueryParam}} @selectedAuth={{this.authMethod}} @onSuccess={{action "onAuthResponse"}} />
|
||||
*
|
||||
* @param {string} wrappedToken - Query param value of a wrapped token that can be used to login when added directly to the URL via the "wrapped_token" query param
|
||||
* @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
|
||||
* @param {string} namespace- Namespace query param, passed to AuthForm and set by typing in namespace input or URL
|
||||
* @param {string} selectedAuth - The auth method selected in the dropdown, passed to auth service's authenticate method
|
||||
* @param {function} onSuccess - Callback that fires the "onAuthResponse" action in the auth controller and handles transitioning after success
|
||||
*/
|
||||
|
||||
export default class AuthLoginFormComponent extends Component {
|
||||
@service auth;
|
||||
|
||||
@tracked authError = null;
|
||||
@tracked oktaNumberChallengeAnswer = '';
|
||||
@tracked waitingForOktaNumberChallenge = false;
|
||||
|
||||
@action
|
||||
performAuth(backendType, data) {
|
||||
this.authenticate.unlinked().perform(backendType, data);
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*delayAuthMessageReminder() {
|
||||
if (Ember.testing) {
|
||||
yield timeout(0);
|
||||
} else {
|
||||
yield timeout(5000);
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*authenticate(backendType, data) {
|
||||
const {
|
||||
selectedAuth,
|
||||
cluster: { id: clusterId },
|
||||
} = this.args;
|
||||
try {
|
||||
if (backendType === 'okta') {
|
||||
this.pollForOktaNumberChallenge.perform(data.nonce, data.path);
|
||||
} else {
|
||||
this.delayAuthMessageReminder.perform();
|
||||
}
|
||||
const authResponse = yield this.auth.authenticate({
|
||||
clusterId,
|
||||
backend: backendType,
|
||||
data,
|
||||
selectedAuth,
|
||||
});
|
||||
|
||||
this.args.onSuccess(authResponse, backendType, data);
|
||||
} catch (e) {
|
||||
if (!this.auth.mfaError) {
|
||||
this.authError = `Authentication failed: ${this.auth.handleError(e)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*pollForOktaNumberChallenge(nonce, mount) {
|
||||
// yield for 1s to wait to see if there is a login error before polling
|
||||
yield timeout(1000);
|
||||
if (this.authError) return;
|
||||
|
||||
this.waitingForOktaNumberChallenge = true;
|
||||
// keep polling /auth/okta/verify/:nonce API every 1s until response returns with correct_number
|
||||
let response = null;
|
||||
while (response === null) {
|
||||
// disable polling for tests otherwise promises reject and acceptance tests fail
|
||||
if (Ember.testing) return;
|
||||
|
||||
yield timeout(1000);
|
||||
response = yield this.auth.getOktaNumberChallengeAnswer(nonce, mount);
|
||||
}
|
||||
// display correct number so user can select on personal MFA device
|
||||
this.oktaNumberChallengeAnswer = response;
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
// reset variables and stop polling tasks if canceling login
|
||||
this.authError = null;
|
||||
this.oktaNumberChallengeAnswer = null;
|
||||
this.waitingForOktaNumberChallenge = false;
|
||||
this.authenticate.cancelAll();
|
||||
this.pollForOktaNumberChallenge.cancelAll();
|
||||
}
|
||||
}
|
||||
@ -3,21 +3,117 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#if this.waitingForOktaNumberChallenge}}
|
||||
<OktaNumberChallenge
|
||||
@correctAnswer={{this.oktaNumberChallengeAnswer}}
|
||||
@hasError={{this.authError}}
|
||||
@onReturnToLogin={{this.onCancel}}
|
||||
/>
|
||||
{{#if this.mfaErrors}}
|
||||
<div class="has-top-margin-xxl" data-test-mfa-error>
|
||||
<EmptyState
|
||||
@title="Unauthorized"
|
||||
@message="Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator."
|
||||
@icon="alert-circle"
|
||||
@bottomBorder={{true}}
|
||||
@subTitle={{join ". " this.mfaErrors}}
|
||||
class="is-shadowless"
|
||||
>
|
||||
<Hds::Button @text="Go back" @icon="chevron-left" @color="tertiary" {{on "click" this.onMfaErrorDismiss}} />
|
||||
</EmptyState>
|
||||
</div>
|
||||
{{else}}
|
||||
<AuthForm
|
||||
@wrappedToken={{@wrappedToken}}
|
||||
@cluster={{@cluster}}
|
||||
@namespace={{@namespace}}
|
||||
@selectedAuth={{@selectedAuth}}
|
||||
@error={{this.authError}}
|
||||
@performAuth={{this.performAuth}}
|
||||
@authIsRunning={{this.authenticate.isRunning}}
|
||||
@delayIsIdle={{this.delayAuthMessageReminder.isIdle}}
|
||||
/>
|
||||
<SplashPage>
|
||||
<:header>
|
||||
{{#if @oidcProviderQueryParam}}
|
||||
<div class="box is-shadowless is-flex-v-centered" data-test-auth-logo>
|
||||
<LogoEdition aria-label="Sign in with Hashicorp Vault" />
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="is-flex-v-centered has-bottom-margin-xxl">
|
||||
<div class="brand-icon-large">
|
||||
<Icon @name="vault" @size="24" @stretched={{true}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-flex-row">
|
||||
{{#if this.mfaAuthData}}
|
||||
<Hds::Button
|
||||
@text="Back to login"
|
||||
@icon="arrow-left"
|
||||
@isIconOnly={{true}}
|
||||
@color="tertiary"
|
||||
{{on "click" (fn (mut this.mfaAuthData) null)}}
|
||||
/>
|
||||
{{/if}}
|
||||
<h1 class="title is-3">
|
||||
{{if this.mfaAuthData "Authenticate" "Sign in to Vault"}}
|
||||
</h1>
|
||||
</div>
|
||||
{{/if}}
|
||||
</:header>
|
||||
|
||||
<:subHeader>
|
||||
{{#if (has-feature "Namespaces")}}
|
||||
{{#unless this.mfaAuthData}}
|
||||
<Toolbar class="toolbar-namespace-picker">
|
||||
<div class="field is-horizontal" data-test-namespace-toolbar>
|
||||
<div class="field-label is-normal">
|
||||
<label class="is-label" for="namespace">Namespace</label>
|
||||
</div>
|
||||
{{#if this.flags.hvdManagedNamespaceRoot}}
|
||||
<div class="field-label">
|
||||
<span class="has-text-grey" data-test-managed-namespace-root>/{{this.flags.hvdManagedNamespaceRoot}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input
|
||||
data-test-auth-form-ns-input
|
||||
value={{this.namespaceInput}}
|
||||
placeholder={{if this.flags.hvdManagedNamespaceRoot "/ (Default)" "/ (Root)"}}
|
||||
{{on "input" this.handleNamespaceUpdate}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
name="namespace"
|
||||
id="namespace"
|
||||
class="input"
|
||||
type="text"
|
||||
disabled={{@oidcProviderQueryParam}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</:subHeader>
|
||||
|
||||
<:content>
|
||||
{{#if this.mfaAuthData}}
|
||||
<Mfa::MfaForm
|
||||
@clusterId={{@cluster.id}}
|
||||
@authData={{this.mfaAuthData}}
|
||||
@onSuccess={{this.onMfaSuccess}}
|
||||
@onError={{fn (mut this.mfaErrors)}}
|
||||
/>
|
||||
{{else}}
|
||||
<Auth::LoginForm
|
||||
@wrappedToken={{@wrappedToken}}
|
||||
@cluster={{@cluster}}
|
||||
@namespace={{@namespaceQueryParam}}
|
||||
@selectedAuth={{@authMethodQueryParam}}
|
||||
@onSuccess={{this.onAuthResponse}}
|
||||
/>
|
||||
{{/if}}
|
||||
</:content>
|
||||
|
||||
<:footer>
|
||||
<div class="has-short-padding">
|
||||
<p class="help has-text-grey-dark" data-test-auth-helptext>
|
||||
{{#if @oidcProviderQueryParam}}
|
||||
Once you log in, you will be redirected back to your application. If you require login credentials, contact your
|
||||
administrator.
|
||||
{{else}}
|
||||
Contact your administrator for login credentials.
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</:footer>
|
||||
</SplashPage>
|
||||
{{/if}}
|
||||
@ -4,10 +4,7 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import Ember from 'ember';
|
||||
import { service } from '@ember/service';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
@ -16,93 +13,60 @@ import { action } from '@ember/object';
|
||||
* The Auth::Page wraps OktaNumberChallenge and AuthForm to manage the login flow and is responsible for calling the authenticate method
|
||||
*
|
||||
* @example
|
||||
* <Auth::Page @wrappedToken={{this.wrappedToken}} @cluster={{this.model}} @namespace={{this.namespaceQueryParam}} @selectedAuth={{this.authMethod}} @onSuccess={{action "onAuthResponse"}} />
|
||||
* <Auth::Page @namespaceQueryParam={{this.namespaceQueryParam}} @onAuthSuccess={{action "authSuccess"}} @oidcProviderQueryParam={{this.oidcProvider}} @cluster={{this.model}} @onNamespaceUpdate={{perform this.updateNamespace}} />
|
||||
*
|
||||
* @param {string} wrappedToken - Query param value of a wrapped token that can be used to login when added directly to the URL via the "wrapped_token" query param
|
||||
* @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
|
||||
* @param {string} namespace- Namespace query param, passed to AuthForm and set by typing in namespace input or URL
|
||||
* @param {string} selectedAuth - The auth method selected in the dropdown, passed to auth service's authenticate method
|
||||
* @param {function} onSuccess - Callback that fires the "onAuthResponse" action in the auth controller and handles transitioning after success
|
||||
*/
|
||||
* @param {string} authMethodQueryParam - auth method type to login with, updated by selecting an auth method from the dropdown
|
||||
* @param {string} namespaceQueryParam - namespace to login with, updated by typing in to the namespace input
|
||||
* @param {string} oidcProviderQueryParam - oidc provider query param, set in url as "?o=someprovider"
|
||||
* @param {function} onAuthSuccess - callback task in controller that receives the auth response (after MFA, if enabled) when login is successful
|
||||
* @param {function} onNamespaceUpdate - callback task that passes user input to the controller to update the login namespace in the url query params
|
||||
* @param {string} wrappedToken - passed down to the AuthForm component and can be used to login if added directly to the URL via the "wrapped_token" query param
|
||||
* */
|
||||
|
||||
export default class AuthPageComponent extends Component {
|
||||
@service auth;
|
||||
export default class AuthPage extends Component {
|
||||
@service flags;
|
||||
|
||||
@tracked authError = null;
|
||||
@tracked oktaNumberChallengeAnswer = '';
|
||||
@tracked waitingForOktaNumberChallenge = false;
|
||||
@tracked mfaErrors;
|
||||
@tracked mfaAuthData;
|
||||
|
||||
@action
|
||||
performAuth(backendType, data) {
|
||||
this.authenticate.unlinked().perform(backendType, data);
|
||||
get namespaceInput() {
|
||||
const namespaceQP = this.args.namespaceQueryParam;
|
||||
if (this.flags.hvdManagedNamespaceRoot) {
|
||||
// When managed, the user isn't allowed to edit the prefix `admin/` for their nested namespace
|
||||
const split = namespaceQP.split('/');
|
||||
if (split.length > 1) {
|
||||
split.shift();
|
||||
return `/${split.join('/')}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return namespaceQP;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*delayAuthMessageReminder() {
|
||||
if (Ember.testing) {
|
||||
yield timeout(0);
|
||||
@action
|
||||
handleNamespaceUpdate(event) {
|
||||
this.args.onNamespaceUpdate(event.target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
onAuthResponse(authResponse, backend, data) {
|
||||
const { mfa_requirement } = authResponse;
|
||||
// if an mfa requirement exists further action is required
|
||||
if (mfa_requirement) {
|
||||
this.mfaAuthData = { mfa_requirement, backend, data };
|
||||
} else {
|
||||
yield timeout(5000);
|
||||
this.args.onAuthSuccess(authResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*authenticate(backendType, data) {
|
||||
const {
|
||||
selectedAuth,
|
||||
cluster: { id: clusterId },
|
||||
} = this.args;
|
||||
try {
|
||||
if (backendType === 'okta') {
|
||||
this.pollForOktaNumberChallenge.perform(data.nonce, data.path);
|
||||
} else {
|
||||
this.delayAuthMessageReminder.perform();
|
||||
}
|
||||
const authResponse = yield this.auth.authenticate({
|
||||
clusterId,
|
||||
backend: backendType,
|
||||
data,
|
||||
selectedAuth,
|
||||
});
|
||||
|
||||
this.args.onSuccess(authResponse, backendType, data);
|
||||
} catch (e) {
|
||||
if (!this.auth.mfaError) {
|
||||
this.authError = `Authentication failed: ${this.auth.handleError(e)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*pollForOktaNumberChallenge(nonce, mount) {
|
||||
// yield for 1s to wait to see if there is a login error before polling
|
||||
yield timeout(1000);
|
||||
if (this.authError) return;
|
||||
|
||||
this.waitingForOktaNumberChallenge = true;
|
||||
// keep polling /auth/okta/verify/:nonce API every 1s until response returns with correct_number
|
||||
let response = null;
|
||||
while (response === null) {
|
||||
// disable polling for tests otherwise promises reject and acceptance tests fail
|
||||
if (Ember.testing) return;
|
||||
|
||||
yield timeout(1000);
|
||||
response = yield this.auth.getOktaNumberChallengeAnswer(nonce, mount);
|
||||
}
|
||||
// display correct number so user can select on personal MFA device
|
||||
this.oktaNumberChallengeAnswer = response;
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
// reset variables and stop polling tasks if canceling login
|
||||
this.authError = null;
|
||||
this.oktaNumberChallengeAnswer = null;
|
||||
this.waitingForOktaNumberChallenge = false;
|
||||
this.authenticate.cancelAll();
|
||||
this.pollForOktaNumberChallenge.cancelAll();
|
||||
onMfaSuccess(authResponse) {
|
||||
this.args.onAuthSuccess(authResponse);
|
||||
}
|
||||
|
||||
@action
|
||||
onMfaErrorDismiss() {
|
||||
this.mfaAuthData = null;
|
||||
this.mfaErrors = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,20 +26,6 @@ export default Controller.extend({
|
||||
authMethod: '',
|
||||
oidcProvider: '',
|
||||
|
||||
get namespaceInput() {
|
||||
const namespaceQP = this.clusterController.namespaceQueryParam;
|
||||
if (this.hvdManagedNamespaceRoot) {
|
||||
// When managed, the user isn't allowed to edit the prefix `admin/` for their nested namespace
|
||||
const split = namespaceQP.split('/');
|
||||
if (split.length > 1) {
|
||||
split.shift();
|
||||
return `/${split.join('/')}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return namespaceQP;
|
||||
},
|
||||
|
||||
fullNamespaceFromInput(value) {
|
||||
const strippedNs = sanitizePath(value);
|
||||
if (this.hvdManagedNamespaceRoot) {
|
||||
@ -57,48 +43,29 @@ export default Controller.extend({
|
||||
this.set('namespaceQueryParam', ns);
|
||||
}).restartable(),
|
||||
|
||||
authSuccess({ isRoot, namespace }) {
|
||||
let transition;
|
||||
this.version.fetchVersion();
|
||||
if (this.redirectTo) {
|
||||
// here we don't need the namespace because it will be encoded in redirectTo
|
||||
transition = this.router.transitionTo(this.redirectTo);
|
||||
// reset the value on the controller because it's bound here
|
||||
this.set('redirectTo', '');
|
||||
} else {
|
||||
transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } });
|
||||
}
|
||||
transition.followRedirects().then(() => {
|
||||
if (this.version.isEnterprise) {
|
||||
this.customMessages.fetchMessages(namespace);
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
this.auth.set('isRootToken', true);
|
||||
this.flashMessages.warning(
|
||||
'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
onAuthResponse(authResponse, backend, data) {
|
||||
const { mfa_requirement } = authResponse;
|
||||
// if an mfa requirement exists further action is required
|
||||
if (mfa_requirement) {
|
||||
this.set('mfaAuthData', { mfa_requirement, backend, data });
|
||||
authSuccess({ isRoot, namespace }) {
|
||||
let transition;
|
||||
this.version.fetchVersion();
|
||||
if (this.redirectTo) {
|
||||
// here we don't need the namespace because it will be encoded in redirectTo
|
||||
transition = this.router.transitionTo(this.redirectTo);
|
||||
// reset the value on the controller because it's bound here
|
||||
this.set('redirectTo', '');
|
||||
} else {
|
||||
this.authSuccess(authResponse);
|
||||
transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } });
|
||||
}
|
||||
},
|
||||
onMfaSuccess(authResponse) {
|
||||
this.authSuccess(authResponse);
|
||||
},
|
||||
onMfaErrorDismiss() {
|
||||
this.setProperties({
|
||||
mfaAuthData: null,
|
||||
mfaErrors: null,
|
||||
transition.followRedirects().then(() => {
|
||||
if (this.version.isEnterprise) {
|
||||
this.customMessages.fetchMessages(namespace);
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
this.auth.set('isRootToken', true);
|
||||
this.flashMessages.warning(
|
||||
'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@ -371,6 +371,7 @@ export default Service.extend({
|
||||
shouldRenew() {
|
||||
const now = this.now();
|
||||
const lastFetch = this.lastFetch;
|
||||
// renewAfterEpoch is a unix timestamp of login time + half of ttl
|
||||
const renewTime = this.renewAfterEpoch;
|
||||
if (!this.currentTokenName || this.tokenExpired || this.allowExpiration || !renewTime) {
|
||||
return false;
|
||||
|
||||
@ -57,9 +57,7 @@
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if this.countdown}}
|
||||
<div>
|
||||
<AlertInline @type="danger" @message={{this.codeDelayMessage}} />
|
||||
</div>
|
||||
<AlertInline @type="danger" class="has-bottom-padding-m" @message={{this.codeDelayMessage}} />
|
||||
{{/if}}
|
||||
<Hds::Button
|
||||
@text="Verify"
|
||||
|
||||
@ -3,117 +3,12 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#if this.mfaErrors}}
|
||||
<div class="has-top-margin-xxl" data-test-mfa-error>
|
||||
<EmptyState
|
||||
@title="Unauthorized"
|
||||
@message="Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator."
|
||||
@icon="alert-circle"
|
||||
@bottomBorder={{true}}
|
||||
@subTitle={{join ". " this.mfaErrors}}
|
||||
class="is-shadowless"
|
||||
>
|
||||
<Hds::Button @text="Go back" @icon="chevron-left" @color="tertiary" {{on "click" (action "onMfaErrorDismiss")}} />
|
||||
</EmptyState>
|
||||
</div>
|
||||
{{else}}
|
||||
<SplashPage>
|
||||
<:header>
|
||||
{{#if this.oidcProvider}}
|
||||
<div class="box is-shadowless is-flex-v-centered" data-test-auth-logo>
|
||||
<LogoEdition aria-label="Sign in with Hashicorp Vault" />
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="is-flex-v-centered has-bottom-margin-xxl">
|
||||
<div class="brand-icon-large">
|
||||
<Icon @name="vault" @size="24" @stretched={{true}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-flex-row">
|
||||
{{#if this.mfaAuthData}}
|
||||
<Hds::Button
|
||||
@text="Back to login"
|
||||
@icon="arrow-left"
|
||||
@isIconOnly={{true}}
|
||||
@color="tertiary"
|
||||
{{on "click" (fn (mut this.mfaAuthData) null)}}
|
||||
/>
|
||||
{{/if}}
|
||||
<h1 class="title is-3">
|
||||
{{if this.mfaAuthData "Authenticate" "Sign in to Vault"}}
|
||||
</h1>
|
||||
</div>
|
||||
{{/if}}
|
||||
</:header>
|
||||
|
||||
<:subHeader>
|
||||
{{#if (has-feature "Namespaces")}}
|
||||
{{#unless this.mfaAuthData}}
|
||||
<Toolbar class="toolbar-namespace-picker">
|
||||
<div class="field is-horizontal" data-test-namespace-toolbar>
|
||||
<div class="field-label is-normal">
|
||||
<label class="is-label" for="namespace">Namespace</label>
|
||||
</div>
|
||||
{{#if this.hvdManagedNamespaceRoot}}
|
||||
<div class="field-label">
|
||||
<span class="has-text-grey" data-test-managed-namespace-root>/{{this.hvdManagedNamespaceRoot}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input
|
||||
data-test-auth-form-ns-input
|
||||
value={{this.namespaceInput}}
|
||||
placeholder={{if this.hvdManagedNamespaceRoot "/ (Default)" "/ (Root)"}}
|
||||
{{on "input" (perform this.updateNamespace value="target.value")}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
name="namespace"
|
||||
id="namespace"
|
||||
class="input"
|
||||
type="text"
|
||||
disabled={{this.oidcProvider}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</:subHeader>
|
||||
|
||||
<:content>
|
||||
{{#if this.mfaAuthData}}
|
||||
<Mfa::MfaForm
|
||||
@clusterId={{this.model.id}}
|
||||
@authData={{this.mfaAuthData}}
|
||||
@onSuccess={{action "onMfaSuccess"}}
|
||||
@onError={{fn (mut this.mfaErrors)}}
|
||||
/>
|
||||
{{else}}
|
||||
<Auth::Page
|
||||
@wrappedToken={{this.wrappedToken}}
|
||||
@cluster={{this.model}}
|
||||
@namespace={{this.namespaceQueryParam}}
|
||||
@selectedAuth={{this.authMethod}}
|
||||
@onSuccess={{action "onAuthResponse"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</:content>
|
||||
|
||||
<:footer>
|
||||
<div class="has-short-padding">
|
||||
<p class="help has-text-grey-dark" data-test-auth-helptext>
|
||||
{{#if this.oidcProvider}}
|
||||
Once you log in, you will be redirected back to your application. If you require login credentials, contact your
|
||||
administrator.
|
||||
{{else}}
|
||||
Contact your administrator for login credentials.
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</:footer>
|
||||
</SplashPage>
|
||||
{{/if}}
|
||||
<Auth::Page
|
||||
@authMethodQueryParam={{this.authMethod}}
|
||||
@cluster={{this.model}}
|
||||
@namespaceQueryParam={{this.namespaceQueryParam}}
|
||||
@oidcProviderQueryParam={{this.oidcProvider}}
|
||||
@onAuthSuccess={{action "authSuccess"}}
|
||||
@onNamespaceUpdate={{perform this.updateNamespace}}
|
||||
@wrappedToken={{this.wrappedToken}}
|
||||
/>
|
||||
@ -3,24 +3,23 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { currentRouteName } from '@ember/test-helpers';
|
||||
import { currentRouteName, visit } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import page from 'vault/tests/pages/access/namespaces/index';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
|
||||
module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
return authPage.login();
|
||||
return login();
|
||||
});
|
||||
|
||||
test('it navigates to namespaces page', async function (assert) {
|
||||
assert.expect(1);
|
||||
await page.visit();
|
||||
await visit('/vault/access/namespaces');
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
'vault.cluster.access.namespaces.index',
|
||||
@ -30,7 +29,7 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
|
||||
|
||||
test('it should render correct number of namespaces', async function (assert) {
|
||||
assert.expect(3);
|
||||
await page.visit();
|
||||
await visit('/vault/access/namespaces');
|
||||
const store = this.owner.lookup('service:store');
|
||||
// Default page size is 15
|
||||
assert.strictEqual(store.peekAll('namespace').length, 15, 'Store has 15 namespaces records');
|
||||
|
||||
20
ui/tests/helpers/auth/auth-helpers.ts
Normal file
20
ui/tests/helpers/auth/auth-helpers.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, visit } from '@ember/test-helpers';
|
||||
import VAULT_KEYS from 'vault/tests/helpers/vault-keys';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
|
||||
const { rootToken } = VAULT_KEYS;
|
||||
|
||||
export const login = async (token = rootToken) => {
|
||||
// make sure we're always logged out and logged back in
|
||||
await visit('/vault/logout');
|
||||
// clear session storage to ensure we have a clean state
|
||||
window.localStorage.clear();
|
||||
await visit('/vault/auth?with=token');
|
||||
await fillIn(AUTH_FORM.input('token'), token);
|
||||
return await click(AUTH_FORM.login);
|
||||
};
|
||||
@ -28,7 +28,7 @@ module('Integration | Component | auth | page ', function (hooks) {
|
||||
|
||||
this.renderComponent = async () => {
|
||||
return render(hbs`
|
||||
<Auth::Page
|
||||
<Auth::LoginForm
|
||||
@wrappedToken={{this.wrappedToken}}
|
||||
@cluster={{this.cluster}}
|
||||
@namespace={{this.namespaceQueryParam}}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { create, visitable } from 'ember-cli-page-object';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/vault/access/namespaces'),
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user