mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
Merge remote-tracking branch 'remotes/from/ce/main'
This commit is contained in:
commit
c1bdf5621d
40 changed files with 549 additions and 683 deletions
|
|
@ -143,7 +143,10 @@
|
|||
{{/each-in}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<EmptyState @title="No plugin selected" @message="Select a plugin type to be able to configure it." />
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32" as |A|>
|
||||
<A.Header data-test-empty-state-title @title="No plugin selected" />
|
||||
<A.Body data-test-empty-state-message @text="Select a plugin type to be able to configure it." />
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
|
@ -154,7 +157,10 @@
|
|||
<div class="form-section box is-shadowless is-fullwidth">
|
||||
<h3 class="title is-5">Statements</h3>
|
||||
{{#if (eq @model.statementFields null)}}
|
||||
<EmptyState @title="No plugin selected" @message="Select a plugin type to be able to configure it." />
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32" as |A|>
|
||||
<A.Header data-test-empty-state-title @title="No plugin selected" />
|
||||
<A.Body data-test-empty-state-message @text="Select a plugin type to be able to configure it." />
|
||||
</Hds::ApplicationState>
|
||||
{{else}}
|
||||
{{#each @model.statementFields as |attr|}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
|
|
@ -302,21 +308,22 @@
|
|||
</div>
|
||||
</form>
|
||||
{{else if (eq @model.isAvailablePlugin false)}}
|
||||
<EmptyState
|
||||
@title="Database type unavailable"
|
||||
@subTitle="Not supported in the UI"
|
||||
@icon="skip"
|
||||
@message="This database type cannot be viewed in the UI. You will have to use the API or CLI to perform actions here."
|
||||
@bottomBorder={{true}}
|
||||
>
|
||||
<Hds::Link::Standalone @icon="chevron-left" @text="Go back" @route="vault.cluster.secrets.backend.list-root" />
|
||||
<Hds::Link::Standalone
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@text="Database API docs"
|
||||
@href={{doc-link "/vault/api-docs/secret/databases"}}
|
||||
/>
|
||||
</EmptyState>
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32" as |A|>
|
||||
<A.Header data-test-empty-state-title @icon="skip" @title="Database type unavailable" />
|
||||
<A.Body data-test-empty-state-message>
|
||||
<p>Not supported in the UI</p>
|
||||
<p>This database type cannot be viewed in the UI. You will have to use the API or CLI to perform actions here.</p>
|
||||
</A.Body>
|
||||
<A.Footer data-test-empty-state-actions as |F|>
|
||||
<F.LinkStandalone @icon="chevron-left" @text="Go back" @route="vault.cluster.secrets.backend.list-root" />
|
||||
<F.LinkStandalone
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@text="Database API docs"
|
||||
@href={{doc-link "/vault/api-docs/secret/databases"}}
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
{{else}}
|
||||
{{#each @model.showAttrs as |attr|}}
|
||||
{{#let attr.options.defaultShown as |defaultDisplay|}}
|
||||
|
|
|
|||
|
|
@ -141,10 +141,10 @@
|
|||
@modelValidations={{this.modelValidations}}
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No database connection selected"
|
||||
@message="Choose a connection to be able to configure a role type."
|
||||
/>
|
||||
<Hds::ApplicationState @align="center" class="bottom-padding-32" as |A|>
|
||||
<A.Header data-test-empty-state-title @title="No database connection selected" />
|
||||
<A.Body data-test-empty-state-message @text="Choose a connection to be able to configure a role type." />
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
<div class="field is-fullwidth box is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@
|
|||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<EmptyState @title="No role type selected" @message="Select a type of role to be able to configure it" />
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32 bottom-padding-32" as |A|>
|
||||
<A.Header data-test-empty-state-title @title="No role type selected" />
|
||||
<A.Body data-test-empty-state-message @text="Select a type of role to be able to configure it" />
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{! template-lint-configure simple-unless "warn" }}
|
||||
|
|
@ -51,10 +54,13 @@
|
|||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No role type selected"
|
||||
@message="Select a type of role to be able to add statements for creation, revocation, and/or rotation."
|
||||
/>
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32 bottom-padding-32" as |A|>
|
||||
<A.Header data-test-empty-state-title @title="No role type selected" />
|
||||
<A.Body
|
||||
data-test-empty-state-message
|
||||
@text="Select a type of role to be able to add statements for creation, revocation, and/or rotation."
|
||||
/>
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
|
@ -12,20 +12,23 @@
|
|||
<div class={{if @roleType "box is-fullwidth is-sideless is-marginless"}}>
|
||||
{{! If no role type, that means both static and dynamic requests returned an error }}
|
||||
{{#unless @roleType}}
|
||||
<EmptyState
|
||||
@title={{this.errorTitle}}
|
||||
@subTitle="Error {{@model.errorHttpStatus}}"
|
||||
@icon="alert-circle"
|
||||
@bottomBorder={{true}}
|
||||
@message={{@model.errorMessage}}
|
||||
>
|
||||
<Hds::Link::Standalone
|
||||
@iconPosition="trailing"
|
||||
@icon="docs-link"
|
||||
@text="Database documentation"
|
||||
@href={{doc-link "/vault/api-docs/secret/databases"}}
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32 bottom-padding-32" as |A|>
|
||||
<A.Header
|
||||
data-test-empty-state-title
|
||||
@title={{this.errorTitle}}
|
||||
@errorCode={{@model.errorHttpStatus}}
|
||||
@icon="alert-circle"
|
||||
/>
|
||||
</EmptyState>
|
||||
<A.Body data-test-empty-state-message @text={{@model.errorMessage}} />
|
||||
<A.Footer data-test-empty-state-actions as |F|>
|
||||
<F.LinkStandalone
|
||||
@iconPosition="trailing"
|
||||
@icon="docs-link"
|
||||
@text="Database documentation"
|
||||
@href={{doc-link "/vault/api-docs/secret/databases"}}
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
{{/unless}}
|
||||
{{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}}
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-bottom-margin" data-test-credentials-warning as |A|>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
import Ember from 'ember';
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { tracked } from 'tracked-built-ins';
|
||||
import type { TrackedSet } from 'tracked-built-ins';
|
||||
import { action } from '@ember/object';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import MfaConstraint from 'vault/resources/mfa/constraint';
|
||||
|
||||
|
|
@ -54,7 +56,7 @@ export default class MfaForm extends Component<Args> {
|
|||
@tracked error = '';
|
||||
// Self-enrollment is per MFA method, not per login enforcement (constraint)
|
||||
// Track method IDs used to fetch a QR code so we don't re-request if a user just enrolled.
|
||||
@tracked enrolledMethods = new Set<string>();
|
||||
enrolledMethods: TrackedSet<string> = tracked(Set);
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
|
|
@ -124,7 +126,7 @@ export default class MfaForm extends Component<Args> {
|
|||
await this.args.loginAndTransition.unlinked().perform(response);
|
||||
} catch (error) {
|
||||
// Reset enrolled methods if there's an error
|
||||
this.enrolledMethods = new Set<string>();
|
||||
this.enrolledMethods.clear();
|
||||
const errorMsg = errorMessage(error);
|
||||
const codeUsed = errorMsg.includes('code already used');
|
||||
const rateLimit = errorMsg.includes('maximum TOTP validation attempts');
|
||||
|
|
@ -141,30 +143,32 @@ export default class MfaForm extends Component<Args> {
|
|||
}
|
||||
});
|
||||
|
||||
fetchQrCode = task(async (mfa_method_id: string, constraint: MfaConstraint) => {
|
||||
// Self-enrollment is an enterprise only feature
|
||||
if (this.version.isCommunity) return;
|
||||
fetchQrCode = task(
|
||||
waitFor(async (mfa_method_id: string, constraint: MfaConstraint) => {
|
||||
// Self-enrollment is an enterprise only feature
|
||||
if (this.version.isCommunity) return;
|
||||
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const { mfaRequirement } = this.args.authData;
|
||||
try {
|
||||
const { data } = await adapter.ajax('/v1/identity/mfa/method/totp/self-enroll', 'POST', {
|
||||
unauthenticated: true,
|
||||
data: { mfa_method_id, mfa_request_id: mfaRequirement.mfa_request_id },
|
||||
});
|
||||
if (data?.url) {
|
||||
// Set QR code which recomputes currentSelfEnrollConstraint and renders it for the user to scan
|
||||
constraint.qrCode = data.url;
|
||||
// Add mfa_method_id to list of already enrolled methods for client-side tracking
|
||||
this.enrolledMethods.add(mfa_method_id);
|
||||
return;
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const { mfaRequirement } = this.args.authData;
|
||||
try {
|
||||
const { data } = await adapter.ajax('/v1/identity/mfa/method/totp/self-enroll', 'POST', {
|
||||
unauthenticated: true,
|
||||
data: { mfa_method_id, mfa_request_id: mfaRequirement.mfa_request_id },
|
||||
});
|
||||
if (data?.url) {
|
||||
// Set QR code which recomputes currentSelfEnrollConstraint and renders it for the user to scan
|
||||
constraint.qrCode = data.url;
|
||||
// Add mfa_method_id to list of already enrolled methods for client-side tracking
|
||||
this.enrolledMethods.add(mfa_method_id);
|
||||
return;
|
||||
}
|
||||
// Not sure it's realistic to get here without the endpoint throwing an error, but just in case!
|
||||
this.error = 'There was a problem generating the QR code. Please try again.';
|
||||
} catch (error) {
|
||||
this.error = errorMessage(error);
|
||||
}
|
||||
// Not sure it's realistic to get here without the endpoint throwing an error, but just in case!
|
||||
this.error = 'There was a problem generating the QR code. Please try again.';
|
||||
} catch (error) {
|
||||
this.error = errorMessage(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
newCodeDelay = task(async (errorMessage) => {
|
||||
let delay;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class VaultController extends Controller {
|
||||
queryParams = [{ redirectTo: 'redirect_to' }];
|
||||
redirectTo = '';
|
||||
@tracked redirectTo = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import config from '../config/environment';
|
||||
|
||||
export function initialize(/* application */) {
|
||||
// attach mount hooks to the environment config
|
||||
// context will be the router DSL
|
||||
config.addRootMounts = function () {
|
||||
this.mount('replication');
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import RSVP from 'rsvp';
|
||||
import {
|
||||
INIT,
|
||||
UNSEAL,
|
||||
AUTH,
|
||||
CLUSTER,
|
||||
CLUSTER_INDEX,
|
||||
OIDC_CALLBACK,
|
||||
OIDC_PROVIDER,
|
||||
NS_OIDC_PROVIDER,
|
||||
DR_REPLICATION_SECONDARY,
|
||||
DR_REPLICATION_SECONDARY_DETAILS,
|
||||
EXCLUDED_REDIRECT_URLS,
|
||||
REDIRECT,
|
||||
} from 'vault/lib/route-paths';
|
||||
|
||||
export default Mixin.create({
|
||||
auth: service(),
|
||||
store: service(),
|
||||
router: service(),
|
||||
|
||||
transitionToTargetRoute(transition = {}) {
|
||||
const targetRoute = this.targetRouteName(transition);
|
||||
if (
|
||||
targetRoute &&
|
||||
targetRoute !== this.routeName &&
|
||||
targetRoute !== transition.targetName &&
|
||||
targetRoute !== this.router.currentRouteName
|
||||
) {
|
||||
// there may be query params so check for inclusion rather than exact match
|
||||
const isExcluded = EXCLUDED_REDIRECT_URLS.find((url) => this.router.currentURL?.includes(url));
|
||||
if (
|
||||
// only want to redirect if we're going to authenticate
|
||||
targetRoute === AUTH &&
|
||||
transition.targetName !== CLUSTER_INDEX &&
|
||||
!isExcluded
|
||||
) {
|
||||
return this.router.transitionTo(targetRoute, {
|
||||
queryParams: { redirect_to: this.router.currentURL },
|
||||
});
|
||||
}
|
||||
return this.router.transitionTo(targetRoute);
|
||||
}
|
||||
|
||||
return RSVP.resolve();
|
||||
},
|
||||
|
||||
beforeModel(transition) {
|
||||
return this.transitionToTargetRoute(transition);
|
||||
},
|
||||
|
||||
clusterModel() {
|
||||
return this.modelFor(CLUSTER) || this.store.peekRecord('cluster', 'vault');
|
||||
},
|
||||
|
||||
authToken() {
|
||||
return this.auth.currentToken;
|
||||
},
|
||||
|
||||
hasKeyData() {
|
||||
/* eslint-disable-next-line ember/no-controller-access-in-routes */
|
||||
return !!this.controllerFor(INIT).keyData;
|
||||
},
|
||||
|
||||
targetRouteName(transition) {
|
||||
const cluster = this.clusterModel();
|
||||
const isAuthed = this.authToken();
|
||||
if (cluster.needsInit) {
|
||||
return INIT;
|
||||
}
|
||||
if (this.hasKeyData() && this.routeName !== UNSEAL && this.routeName !== AUTH) {
|
||||
return INIT;
|
||||
}
|
||||
if (cluster.sealed) {
|
||||
return UNSEAL;
|
||||
}
|
||||
if (cluster?.dr?.isSecondary) {
|
||||
if (transition && transition.targetName === DR_REPLICATION_SECONDARY_DETAILS) {
|
||||
return DR_REPLICATION_SECONDARY_DETAILS;
|
||||
}
|
||||
if (this.router.currentRouteName === DR_REPLICATION_SECONDARY_DETAILS) {
|
||||
return DR_REPLICATION_SECONDARY_DETAILS;
|
||||
}
|
||||
|
||||
return DR_REPLICATION_SECONDARY;
|
||||
}
|
||||
if (!isAuthed) {
|
||||
if ((transition && transition.targetName === OIDC_PROVIDER) || this.routeName === OIDC_PROVIDER) {
|
||||
return OIDC_PROVIDER;
|
||||
}
|
||||
if ((transition && transition.targetName === NS_OIDC_PROVIDER) || this.routeName === NS_OIDC_PROVIDER) {
|
||||
return NS_OIDC_PROVIDER;
|
||||
}
|
||||
if ((transition && transition.targetName === OIDC_CALLBACK) || this.routeName === OIDC_CALLBACK) {
|
||||
return OIDC_CALLBACK;
|
||||
}
|
||||
return AUTH;
|
||||
}
|
||||
if (
|
||||
(!cluster.needsInit && this.routeName === INIT) ||
|
||||
(!cluster.sealed && this.routeName === UNSEAL) ||
|
||||
(!cluster?.dr?.isSecondary && this.routeName === DR_REPLICATION_SECONDARY)
|
||||
) {
|
||||
return CLUSTER;
|
||||
}
|
||||
if (isAuthed && this.routeName === AUTH) {
|
||||
// if you're already authed and you wanna go to auth, you probably want to redirect
|
||||
return REDIRECT;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// meant for use mixed-in to a Route file
|
||||
//
|
||||
// When a route is deactivated, this mixin clears the Ember Data store of
|
||||
// models of type specified by the required param `modelType`.
|
||||
//
|
||||
// example:
|
||||
// Using this as with a modelType of `datacenter` on the infrastructure
|
||||
// route will cause all `datacenter` models to get unloaded when the
|
||||
// infrastructure route is navigated away from.
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { warn } from '@ember/debug';
|
||||
import { on } from '@ember/object/evented';
|
||||
import Mixin from '@ember/object/mixin';
|
||||
|
||||
export default Mixin.create({
|
||||
modelType: null,
|
||||
modelTypes: null,
|
||||
|
||||
verifyProps: on('init', function () {
|
||||
var modelType = this.modelType;
|
||||
var modelTypes = this.modelTypes;
|
||||
warn(
|
||||
'No `modelType` or `modelTypes` specified for `' +
|
||||
this.toString() +
|
||||
'`. Check to make sure you still need to use the `model-boundary-route` mixin.',
|
||||
isPresent(modelType) || isPresent(modelTypes),
|
||||
{ id: 'model-boundary-init' }
|
||||
);
|
||||
|
||||
warn(
|
||||
'Expected `model-boundary-route` to be used on an Ember.Route, not `' + this.toString() + '`.',
|
||||
this instanceof Route,
|
||||
{ id: 'mode-boundary-is-route' }
|
||||
);
|
||||
}),
|
||||
|
||||
clearModelCache: on('deactivate', function () {
|
||||
var modelType = this.modelType;
|
||||
var modelTypes = this.modelTypes;
|
||||
|
||||
if (!modelType && !modelTypes) {
|
||||
warn(
|
||||
'Attempted to clear store clear store cache when leaving `' +
|
||||
this.routeName +
|
||||
'`, but no `modelType` or `modelTypes` was specified.',
|
||||
isPresent(modelType),
|
||||
{ id: 'model-boundary-clear' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.store.isDestroyed || this.store.isDestroying) {
|
||||
// Prevent unload attempt after test teardown, resulting in test failure
|
||||
return;
|
||||
}
|
||||
|
||||
if (modelType) {
|
||||
this.store.unloadAll(modelType);
|
||||
}
|
||||
if (modelTypes) {
|
||||
modelTypes.forEach((type) => {
|
||||
this.store.unloadAll(type);
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
@ -223,10 +223,7 @@ Router.map(function () {
|
|||
this.route('replication-dr-promote', function () {
|
||||
this.route('details');
|
||||
});
|
||||
if (config.addRootMounts) {
|
||||
config.addRootMounts.call(this);
|
||||
}
|
||||
|
||||
this.mount('replication');
|
||||
this.route('not-found', { path: '/*path' });
|
||||
});
|
||||
this.route('not-found', { path: '/*path' });
|
||||
|
|
|
|||
|
|
@ -4,19 +4,37 @@
|
|||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import { reject } from 'rsvp';
|
||||
import Route from '@ember/routing/route';
|
||||
import { reject } from 'rsvp';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import Ember from 'ember';
|
||||
import getStorage from '../../lib/token-storage';
|
||||
import localStorage from 'vault/lib/local-storage';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
|
||||
import clearModelCache from 'vault/utils/shared-model-boundary';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
INIT,
|
||||
UNSEAL,
|
||||
AUTH,
|
||||
CLUSTER_INDEX,
|
||||
OIDC_CALLBACK,
|
||||
OIDC_PROVIDER,
|
||||
NS_OIDC_PROVIDER,
|
||||
DR_REPLICATION_SECONDARY,
|
||||
DR_REPLICATION_SECONDARY_DETAILS,
|
||||
EXCLUDED_REDIRECT_URLS,
|
||||
} from 'vault/lib/route-paths';
|
||||
|
||||
const CLUSTER_STATE = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
SEALED: 'sealed',
|
||||
DR_SECONDARY: 'dr-secondary',
|
||||
ACTIVE: 'active',
|
||||
};
|
||||
|
||||
const POLL_INTERVAL_MS = 10000;
|
||||
|
||||
export const getManagedNamespace = (nsParam, root) => {
|
||||
|
|
@ -29,34 +47,33 @@ export const getManagedNamespace = (nsParam, root) => {
|
|||
return `${root}/${nsParam}`;
|
||||
};
|
||||
|
||||
export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
auth: service(),
|
||||
api: service(),
|
||||
analytics: service(),
|
||||
currentCluster: service(),
|
||||
customMessages: service(),
|
||||
flagsService: service('flags'),
|
||||
namespaceService: service('namespace'),
|
||||
permissions: service(),
|
||||
router: service(),
|
||||
store: service(),
|
||||
version: service(),
|
||||
modelTypes: computed(function () {
|
||||
return ['node', 'secret', 'secret-engine'];
|
||||
}),
|
||||
export default class ClusterRoute extends Route {
|
||||
@service auth;
|
||||
@service api;
|
||||
@service analytics;
|
||||
@service currentCluster;
|
||||
@service customMessages;
|
||||
@service('flags') flagsService;
|
||||
@service('namespace') namespaceService;
|
||||
@service permissions;
|
||||
@service router;
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
queryParams: {
|
||||
modelTypes = ['node', 'secret', 'secret-engine'];
|
||||
|
||||
queryParams = {
|
||||
namespaceQueryParam: {
|
||||
refreshModel: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
getClusterId(params) {
|
||||
const { cluster_name } = params;
|
||||
const records = this.store.peekAll('cluster');
|
||||
const cluster = records.find((record) => record.name === cluster_name);
|
||||
return cluster?.id ?? null;
|
||||
},
|
||||
}
|
||||
|
||||
async beforeModel() {
|
||||
const params = this.paramsFor(this.routeName);
|
||||
|
|
@ -98,7 +115,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
} else {
|
||||
return reject({ httpStatus: 404, message: 'not found', path: params.cluster_name });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
model(params) {
|
||||
// if a user's browser settings block localStorage they will be unable to use Vault. The method will throw the error and the rest of the application will not load.
|
||||
|
|
@ -106,9 +123,10 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
|
||||
const id = this.getClusterId(params);
|
||||
return this.store.findRecord('cluster', id);
|
||||
},
|
||||
}
|
||||
|
||||
poll: task(function* () {
|
||||
poll = task({ keepLatest: true }, async () => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// In test mode, polling causes acceptance tests to hang due to never-settling promises.
|
||||
// To avoid this, polling is disabled during tests.
|
||||
|
|
@ -117,22 +135,20 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
if (Ember.testing) {
|
||||
return;
|
||||
}
|
||||
yield timeout(POLL_INTERVAL_MS);
|
||||
await timeout(POLL_INTERVAL_MS);
|
||||
try {
|
||||
/* eslint-disable-next-line ember/no-controller-access-in-routes */
|
||||
yield this.controller.model.reload();
|
||||
yield this.transitionToTargetRoute();
|
||||
await this.controller.model.reload();
|
||||
await this.transitionToTargetRoute();
|
||||
} catch (e) {
|
||||
// we want to keep polling here
|
||||
}
|
||||
}
|
||||
})
|
||||
.cancelOn('deactivate')
|
||||
.keepLatest(),
|
||||
});
|
||||
|
||||
// Note: do not make this afterModel hook async, it will break the DR secondary flow.
|
||||
afterModel(model, transition) {
|
||||
this._super(...arguments);
|
||||
super.afterModel(...arguments);
|
||||
|
||||
this.currentCluster.setCluster(model);
|
||||
if (model.needsInit && this.auth.currentToken) {
|
||||
|
|
@ -156,7 +172,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
this.addAnalyticsService(model);
|
||||
|
||||
return this.transitionToTargetRoute(transition);
|
||||
},
|
||||
}
|
||||
|
||||
async addAnalyticsService(model) {
|
||||
// identify user for analytics service
|
||||
|
|
@ -187,19 +203,91 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
console.error('unable to start analytics', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
clearModelCache(this.store, this.modelTypes);
|
||||
this.poll.cancelAll();
|
||||
}
|
||||
|
||||
setupController() {
|
||||
this._super(...arguments);
|
||||
super.setupController(...arguments);
|
||||
this.poll.perform();
|
||||
},
|
||||
}
|
||||
|
||||
actions: {
|
||||
error(e) {
|
||||
if (e.httpStatus === 503 && e.errors[0] === 'Vault is sealed') {
|
||||
this.refresh();
|
||||
error = (e) => {
|
||||
if (e.httpStatus === 503 && e.errors[0] === 'Vault is sealed') {
|
||||
this.refresh();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
get clusterState() {
|
||||
const cluster = this.modelFor(this.routeName);
|
||||
switch (true) {
|
||||
case cluster.needsInit:
|
||||
return CLUSTER_STATE.UNINITIALIZED;
|
||||
case cluster.sealed:
|
||||
return CLUSTER_STATE.SEALED;
|
||||
case cluster.dr?.isSecondary:
|
||||
return CLUSTER_STATE.DR_SECONDARY;
|
||||
default:
|
||||
return CLUSTER_STATE.ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
get allowedRedirectRoutes() {
|
||||
const state = this.clusterState;
|
||||
const isAuthenticated = !!this.auth.currentToken;
|
||||
|
||||
switch (true) {
|
||||
case state === CLUSTER_STATE.UNINITIALIZED:
|
||||
return [INIT];
|
||||
case state === CLUSTER_STATE.SEALED:
|
||||
// here we allow init so that we don't redirect users who haven't downloaded keys after init
|
||||
return [UNSEAL, INIT];
|
||||
case state === CLUSTER_STATE.DR_SECONDARY:
|
||||
return [DR_REPLICATION_SECONDARY, DR_REPLICATION_SECONDARY_DETAILS];
|
||||
case isAuthenticated === false:
|
||||
return [AUTH, OIDC_PROVIDER, NS_OIDC_PROVIDER, OIDC_CALLBACK];
|
||||
default:
|
||||
return [null];
|
||||
}
|
||||
}
|
||||
|
||||
targetRouteName(transition) {
|
||||
const currentRouteName = this.router.currentRouteName;
|
||||
const allowedRoutes = this.allowedRedirectRoutes;
|
||||
|
||||
// if the target route or current route is in the allowed routes, go there, otherwise choose the first allowed route
|
||||
return (
|
||||
allowedRoutes?.find((r) => transition?.targetName === r || currentRouteName === r) || allowedRoutes[0]
|
||||
);
|
||||
}
|
||||
|
||||
transitionToTargetRoute(transition = {}) {
|
||||
const targetRoute = this.targetRouteName(transition);
|
||||
if (
|
||||
targetRoute &&
|
||||
targetRoute !== this.routeName &&
|
||||
targetRoute !== transition.targetName &&
|
||||
targetRoute !== this.router.currentRouteName
|
||||
) {
|
||||
// there may be query params so check for inclusion rather than exact match
|
||||
const isExcluded = EXCLUDED_REDIRECT_URLS.find((url) => this.router.currentURL?.includes(url));
|
||||
if (
|
||||
// only want to redirect if we're going to authenticate
|
||||
targetRoute === AUTH &&
|
||||
transition.targetName !== CLUSTER_INDEX &&
|
||||
!isExcluded
|
||||
) {
|
||||
return this.router.transitionTo(targetRoute, {
|
||||
queryParams: { redirect_to: this.router.currentURL },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
return this.router.transitionTo(targetRoute);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { computed } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
|
||||
import clearModelCache from 'vault/utils/shared-model-boundary';
|
||||
|
||||
export default class AccessRoute extends Route {
|
||||
@service store;
|
||||
|
||||
modelTypes = ['capabilities', 'control-group', 'identity/group', 'identity/group-alias', 'identity/alias'];
|
||||
|
||||
export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
modelTypes: computed(function () {
|
||||
return ['capabilities', 'control-group', 'identity/group', 'identity/group-alias', 'identity/alias'];
|
||||
}),
|
||||
model() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
clearModelCache(this.store, this.modelTypes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
store: service(),
|
||||
export default class LeasesRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
return this.store.findRecord('capabilities', 'sys/leases/lookup/');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import ClusterRouteBase from './cluster-route-base';
|
||||
import Route from '@ember/routing/route';
|
||||
import config from 'vault/config/environment';
|
||||
import { supportedTypes } from 'vault/utils/auth-form-helpers';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
import AuthMethodResource from 'vault/resources/auth/method';
|
||||
import { REDIRECT } from 'vault/lib/route-paths';
|
||||
|
||||
export default class AuthRoute extends ClusterRouteBase {
|
||||
export default class AuthRoute extends Route {
|
||||
queryParams = {
|
||||
authMount: { replace: true, refreshModel: true },
|
||||
wrapped_token: { refreshModel: true },
|
||||
|
|
@ -20,13 +21,15 @@ export default class AuthRoute extends ClusterRouteBase {
|
|||
@service auth;
|
||||
@service flashMessages;
|
||||
@service namespace;
|
||||
@service router;
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
beforeModel() {
|
||||
return super.beforeModel().then(() => {
|
||||
return this.version.fetchFeatures();
|
||||
});
|
||||
if (this.auth.currentToken) {
|
||||
return this.router.replaceWith(REDIRECT);
|
||||
}
|
||||
return this.version.fetchFeatures();
|
||||
}
|
||||
|
||||
async model(params) {
|
||||
|
|
@ -62,13 +65,14 @@ export default class AuthRoute extends ClusterRouteBase {
|
|||
redirect(model, transition) {
|
||||
if (model?.unwrapResponse) {
|
||||
// handles the transition
|
||||
/* eslint-disable-next-line ember/no-controller-access-in-routes */
|
||||
return this.controllerFor('vault.cluster.auth').loginAndTransition.perform(model.unwrapResponse);
|
||||
}
|
||||
const hasQueryParam = transition.to?.queryParams?.with;
|
||||
const isInvalid = !model.directLinkData;
|
||||
if (hasQueryParam && isInvalid) {
|
||||
// redirect user and clear out the query param if it's invalid
|
||||
this.router.replaceWith(this.routeName, { queryParams: { authMount: null } });
|
||||
return this.router.replaceWith(this.routeName, { queryParams: { authMount: '' } });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +84,7 @@ export default class AuthRoute extends ClusterRouteBase {
|
|||
return await this.auth.authSuccess(clusterId, authData);
|
||||
} catch (e) {
|
||||
const { message } = await this.api.parseError(e);
|
||||
/* eslint-disable-next-line ember/no-controller-access-in-routes */
|
||||
this.controllerFor('vault.cluster.auth').unwrapTokenError = message;
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +130,7 @@ export default class AuthRoute extends ClusterRouteBase {
|
|||
|
||||
/*
|
||||
In older versions of Vault, the "with" query param could refer to either the auth mount path or the type
|
||||
(which may be the same, since the default mount path *is* the type).
|
||||
(which may be the same, since the default mount path *is* the type).
|
||||
For backward compatibility, we handle both scenarios.
|
||||
→ If `authMount` matches a visible auth mount the method will assume that mount path to login and render as the default in the login form.
|
||||
→ If `authMount` matches a supported auth type (and the mount does not have `listing_visibility="unauth"`), that type is preselected in the login form.
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@
|
|||
// all of the CLUSTER_ROUTES that are states before you can use vault
|
||||
//
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
/**
|
||||
* @type Class
|
||||
*/
|
||||
export default Route.extend(ClusterRoute, {
|
||||
export default class ClusterBaseRoute extends Route {
|
||||
model() {
|
||||
return this.modelFor('vault.cluster');
|
||||
},
|
||||
}
|
||||
|
||||
resetController(controller) {
|
||||
controller.reset && controller.reset();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
// eslint-disable-next-line ember/no-mixins
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import { action } from '@ember/object';
|
||||
import Route from '@ember/routing/route';
|
||||
import SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
|
||||
export default class VaultClusterDashboardRoute extends Route.extend(ClusterRoute) {
|
||||
export default class VaultClusterDashboardRoute extends Route {
|
||||
@service store;
|
||||
@service namespace;
|
||||
@service version;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ClusterRoute from './cluster-route-base';
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { CLUSTER } from 'vault/lib/route-paths';
|
||||
|
||||
export default ClusterRoute;
|
||||
export default class InitRoute extends Route {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
const cluster = this.modelFor(CLUSTER);
|
||||
// if it doesn't need init, nav to cluster route
|
||||
if (!cluster.needsInit) {
|
||||
return this.router.replaceWith(CLUSTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,21 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
store: service(),
|
||||
version: service(),
|
||||
router: service(),
|
||||
export default class LicenseRoute extends Route {
|
||||
@service store;
|
||||
@service version;
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
if (this.version.isCommunity) {
|
||||
this.router.transitionTo('vault.cluster');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
model() {
|
||||
return this.store.queryRecord('license', {});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,25 +4,23 @@
|
|||
*/
|
||||
|
||||
import Ember from 'ember';
|
||||
import { computed } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
|
||||
import clearModelCache from 'vault/utils/shared-model-boundary';
|
||||
|
||||
export default Route.extend(ModelBoundaryRoute, {
|
||||
auth: service(),
|
||||
controlGroup: service(),
|
||||
flashMessages: service(),
|
||||
console: service(),
|
||||
permissions: service(),
|
||||
namespaceService: service('namespace'),
|
||||
router: service(),
|
||||
version: service(),
|
||||
customMessages: service(),
|
||||
export default class LogoutRoute extends Route {
|
||||
@service auth;
|
||||
@service store;
|
||||
@service controlGroup;
|
||||
@service flashMessages;
|
||||
@service console;
|
||||
@service permissions;
|
||||
@service('namespace') namespaceService;
|
||||
@service router;
|
||||
@service version;
|
||||
@service customMessages;
|
||||
|
||||
modelTypes: computed(function () {
|
||||
return ['secret', 'secret-engine'];
|
||||
}),
|
||||
modelTypes = ['secret', 'secret-engine'];
|
||||
|
||||
beforeModel({ to: { queryParams } }) {
|
||||
const ns = this.namespaceService.path;
|
||||
|
|
@ -49,5 +47,8 @@ export default Route.extend(ModelBoundaryRoute, {
|
|||
const { cluster_name } = this.paramsFor('vault.cluster');
|
||||
location.assign(this.router.urlFor('vault.cluster.auth', cluster_name, { queryParams }));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
deactivate() {
|
||||
clearModelCache(this.store, this.modelTypes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,17 @@
|
|||
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
const ALLOWED_TYPES = ['acl', 'egp', 'rgp'];
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
version: service(),
|
||||
export default class PoliciesRoute extends Route {
|
||||
@service version;
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
return this.version.fetchFeatures().then(() => {
|
||||
return this._super(...arguments);
|
||||
return super.beforeModel(...arguments);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
model(params) {
|
||||
const policyType = params.type;
|
||||
|
|
@ -24,5 +23,5 @@ export default Route.extend(ClusterRoute, {
|
|||
return this.router.transitionTo(this.routeName, ALLOWED_TYPES[0]);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import ListRoute from 'core/mixins/list-route';
|
||||
|
||||
export default Route.extend(ClusterRoute, ListRoute, {
|
||||
export default Route.extend(ListRoute, {
|
||||
pagination: service(),
|
||||
version: service(),
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
|
||||
import { service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
const ALLOWED_TYPES = ['acl', 'egp', 'rgp'];
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
version: service(),
|
||||
export default class PolicyRoute extends Route {
|
||||
@service version;
|
||||
@service router;
|
||||
beforeModel() {
|
||||
return this.version.fetchFeatures().then(() => {
|
||||
return this._super(...arguments);
|
||||
return super.beforeModel(...arguments);
|
||||
});
|
||||
},
|
||||
}
|
||||
model(params) {
|
||||
const policyType = params.type;
|
||||
if (!ALLOWED_TYPES.includes(policyType)) {
|
||||
|
|
@ -25,5 +25,5 @@ export default Route.extend(ClusterRoute, {
|
|||
return this.router.transitionTo('vault.cluster.policies', policyType);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
ui/app/routes/vault/cluster/replication-dr-promote.js
Normal file
20
ui/app/routes/vault/cluster/replication-dr-promote.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { CLUSTER } from 'vault/lib/route-paths';
|
||||
|
||||
export default class ReplicationDrPromote extends Route {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
const cluster = this.modelFor(CLUSTER);
|
||||
// if it's not a dr secondary, go back to cluster
|
||||
if (!cluster?.dr?.isSecondary) {
|
||||
return this.router.replaceWith(CLUSTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,11 @@
|
|||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Base from '../cluster-route-base';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Base.extend({
|
||||
replicationMode: service(),
|
||||
export default class ReplicationDetailsRoute extends Route {
|
||||
@service replicationMode;
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.replicationMode.setMode('dr');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@
|
|||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Base from '../cluster-route-base';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Base.extend({
|
||||
replicationMode: service(),
|
||||
export default class ReplicationIndexRoute extends Route {
|
||||
@service replicationMode;
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.replicationMode.setMode('dr');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
export default Route.extend(ClusterRoute);
|
||||
|
|
@ -4,10 +4,9 @@
|
|||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
export default class SettingsRoute extends Route {
|
||||
model() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,15 @@
|
|||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
store: service(),
|
||||
export default class StorageRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
// findAll method will return all records in store as well as response from server
|
||||
// when removing a peer via the cli, stale records would continue to appear until refresh
|
||||
// query method will only return records from response
|
||||
return this.store.query('server', {});
|
||||
},
|
||||
|
||||
actions: {
|
||||
doRefresh() {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@
|
|||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
export default class ToolsRoute extends Route {
|
||||
model() {
|
||||
return this.modelFor('vault.cluster');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ClusterRoute from './cluster-route-base';
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { CLUSTER } from 'vault/lib/route-paths';
|
||||
|
||||
export default ClusterRoute.extend({});
|
||||
export default class UnsealRoute extends Route {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
const cluster = this.modelFor(CLUSTER);
|
||||
// if it's not sealed, we don't need to unseal
|
||||
if (!cluster.sealed) {
|
||||
return this.router.replaceWith(CLUSTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
ui/app/utils/shared-model-boundary.js
Normal file
18
ui/app/utils/shared-model-boundary.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export default function clearModelCache(store, modelType) {
|
||||
if (!modelType) {
|
||||
return;
|
||||
}
|
||||
const modelTypes = Array.isArray(modelType) ? modelType : [modelType];
|
||||
if (store.isDestroyed || store.isDestroying) {
|
||||
// Prevent unload attempt after test teardown, resulting in test failure
|
||||
return;
|
||||
}
|
||||
modelTypes.forEach((type) => {
|
||||
store.unloadAll(type);
|
||||
});
|
||||
}
|
||||
|
|
@ -4,16 +4,14 @@
|
|||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import { setProperties } from '@ember/object';
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
version: service(),
|
||||
store: service(),
|
||||
auth: service(),
|
||||
router: service('app-router'),
|
||||
capabilities: service(),
|
||||
export default class ApplicationRoute extends Route {
|
||||
@service version;
|
||||
@service store;
|
||||
@service auth;
|
||||
@service('app-router') router;
|
||||
@service capabilities;
|
||||
|
||||
async fetchCapabilities() {
|
||||
const enablePath = (type, cluster) => `sys/replication/${type}/${cluster}/enable`;
|
||||
|
|
@ -29,21 +27,19 @@ export default Route.extend(ClusterRoute, {
|
|||
canEnablePrimaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
|
||||
canEnableSecondaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
beforeModel() {
|
||||
if (this.auth.activeCluster.replicationRedacted) {
|
||||
// disallow replication access if endpoints are redacted
|
||||
return this.router.transitionTo('vault.cluster');
|
||||
}
|
||||
return this.version.fetchFeatures().then(() => {
|
||||
return this._super(...arguments);
|
||||
});
|
||||
},
|
||||
return this.version.fetchFeatures();
|
||||
}
|
||||
|
||||
model() {
|
||||
return this.auth.activeCluster;
|
||||
},
|
||||
}
|
||||
|
||||
async afterModel(model) {
|
||||
const {
|
||||
|
|
@ -53,17 +49,10 @@ export default Route.extend(ClusterRoute, {
|
|||
canEnableSecondaryPerformance,
|
||||
} = await this.fetchCapabilities();
|
||||
|
||||
setProperties(model, {
|
||||
canEnablePrimaryDr,
|
||||
canEnableSecondaryDr,
|
||||
canEnablePrimaryPerformance,
|
||||
canEnableSecondaryPerformance,
|
||||
});
|
||||
model.canEnablePrimaryDr = canEnablePrimaryDr;
|
||||
model.canEnableSecondaryDr = canEnableSecondaryDr;
|
||||
model.canEnablePrimaryPerformance = canEnablePrimaryPerformance;
|
||||
model.canEnableSecondaryPerformance = canEnableSecondaryPerformance;
|
||||
return model;
|
||||
},
|
||||
actions: {
|
||||
refresh() {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,4 @@ export default Route.extend({
|
|||
return cluster;
|
||||
});
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
const replicationMode = this.paramsFor('mode').replication_mode;
|
||||
const cluster = model[replicationMode];
|
||||
if (!cluster.isPrimary || cluster.replicationDisabled || cluster.replicationUnsupported) {
|
||||
return this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ module('Acceptance | auth login', function (hooks) {
|
|||
assert.expect(6);
|
||||
this.authType = 'oidc';
|
||||
this.expectedPayload = {
|
||||
redirect_uri: 'http://localhost:7357/ui/vault/auth/custom-oidc/oidc/callback',
|
||||
redirect_uri: `${window.location.origin}/ui/vault/auth/custom-oidc/oidc/callback`,
|
||||
role: 'some-dev',
|
||||
};
|
||||
this.server.post('/auth/custom-oidc/oidc/auth_url', (schema, req) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,16 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, findAll, currentURL, visit, settled, waitFor } from '@ember/test-helpers';
|
||||
import {
|
||||
click,
|
||||
fillIn,
|
||||
findAll,
|
||||
currentURL,
|
||||
currentRouteName,
|
||||
visit,
|
||||
settled,
|
||||
waitFor,
|
||||
} from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
|
|
@ -84,7 +93,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||
assert.ok(findAll('[data-test-demote-warning]').length, 'displays the demotion warning');
|
||||
});
|
||||
|
||||
test('DR primary: shows empty state when secondary mode is not enabled and we navigated to the secondary details page', async function (assert) {
|
||||
test('DR primary: redirects to cluster route when navigating to the secondary details page', async function (assert) {
|
||||
// enable dr replication
|
||||
|
||||
await visit('/vault/replication/dr');
|
||||
|
|
@ -94,16 +103,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||
await settled(); // eslint-disable-line
|
||||
await pollCluster(this.owner);
|
||||
await visit('/vault/replication-dr-promote/details');
|
||||
|
||||
assert
|
||||
.dom('[data-test-component="empty-state"]')
|
||||
.exists('Empty state is shown when no secondary is configured');
|
||||
assert
|
||||
.dom('[data-test-empty-state-message]')
|
||||
.hasText(
|
||||
'This Disaster Recovery secondary has not been enabled. You can do so from the Disaster Recovery Primary.',
|
||||
'Renders the correct message for when a primary is enabled but no secondary is configured and we have navigated to the secondary details page.'
|
||||
);
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'redirects to the cluster route');
|
||||
});
|
||||
|
||||
test('DR primary: runs analytics service when enabled', async function (assert) {
|
||||
|
|
@ -126,22 +126,6 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||
addAnalyticsSpy.restore();
|
||||
});
|
||||
|
||||
test('DR secondary: shows empty state when replication is not enabled', async function (assert) {
|
||||
await visit('/vault/replication-dr-promote/details');
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').exists();
|
||||
assert
|
||||
.dom(GENERAL.emptyStateTitle)
|
||||
.includesText('Disaster Recovery secondary not set up', 'shows the correct title of the empty state');
|
||||
|
||||
assert
|
||||
.dom(GENERAL.emptyStateMessage)
|
||||
.hasText(
|
||||
'This cluster has not been enabled as a Disaster Recovery Secondary. You can do so by enabling replication and adding a secondary from the Disaster Recovery Primary.',
|
||||
'renders default message specific to when no replication is enabled'
|
||||
);
|
||||
});
|
||||
|
||||
test('Performance primary: add secondary and delete config', async function (assert) {
|
||||
const secondaryName = `performanceSecondary`;
|
||||
const mode = 'deny';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { click, currentURL, visit } from '@ember/test-helpers';
|
||||
import { click, currentURL } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
|
||||
|
|
@ -16,12 +16,12 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
|
|||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
return login();
|
||||
hooks.beforeEach(async function () {
|
||||
await login();
|
||||
});
|
||||
|
||||
// common links are tested in the sidebar-nav test and will not be covered here
|
||||
test('it should render enterprise only navigation links', async function (assert) {
|
||||
test(`it should render enterprise only navigation links`, async function (assert) {
|
||||
assert.dom(panel('Cluster')).exists('Cluster nav panel renders');
|
||||
|
||||
await click(link('Secrets Sync'));
|
||||
|
|
@ -41,12 +41,9 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
|
|||
'Replication performance route renders'
|
||||
);
|
||||
|
||||
// for some reason clicking this link would cause the testing browser locally
|
||||
// to navigate to 'vault/replication/dr' and halt the test runner
|
||||
assert
|
||||
.dom(link('Disaster Recovery'))
|
||||
.hasAttribute('href', '/ui/vault/replication/dr', 'Replication dr route renders');
|
||||
await visit('/vault');
|
||||
await click(link('Disaster Recovery'));
|
||||
assert.strictEqual(currentURL(), '/vault/replication/dr', 'Replication DR route renders');
|
||||
await click(link('Back to main navigation'));
|
||||
|
||||
await click(link('Client Count'));
|
||||
assert.dom(panel('Client Count')).exists('Client Count nav panel renders');
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { setupRenderingTest } from 'ember-qunit';
|
|||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
|
|
@ -96,7 +97,7 @@ module('Integration | Component | database-role-setting-form', function (hooks)
|
|||
},
|
||||
});
|
||||
await render(hbs`<DatabaseRoleSettingForm @attrs={{this.model.attrs}} @model={{this.model}}/>`);
|
||||
assert.dom('[data-test-component="empty-state"]').exists({ count: 2 }, 'Two empty states exist');
|
||||
assert.dom(GENERAL.emptyStateTitle).exists({ count: 2 }, 'Two empty states exist');
|
||||
});
|
||||
|
||||
test('it shows appropriate fields based on roleType and db plugin', async function (assert) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ module('Integration | Component | open-api-explorer | swagger-ui', function (hoo
|
|||
|
||||
this.totalApiPaths = Object.keys(this.openApiResponse.paths).length;
|
||||
|
||||
this.renderComponent = () => render(hbs`<SwaggerUi/>`, { owner: this.engine });
|
||||
this.renderComponent = async () => {
|
||||
await render(hbs`<SwaggerUi/>`, { owner: this.engine });
|
||||
await waitFor(SELECTORS.searchInput);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
|
|
|
|||
125
ui/tests/integration/routes/cluster-route-test.js
Normal file
125
ui/tests/integration/routes/cluster-route-test.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ClusterRoute from 'vault/routes/vault/cluster';
|
||||
import { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY } from 'vault/lib/route-paths';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Route | ClusterRoute', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
function createClusterRoute(
|
||||
context,
|
||||
clusterModel = {},
|
||||
methods = {
|
||||
auth: { currentToken: null },
|
||||
}
|
||||
) {
|
||||
context.owner.register('route:cluster-route-test', ClusterRoute);
|
||||
const instance = context.owner.lookup('route:cluster-route-test');
|
||||
for (const key of Object.keys(methods)) {
|
||||
if (typeof methods[key] === 'function') {
|
||||
sinon.stub(instance, key).callsFake(methods[key]);
|
||||
}
|
||||
}
|
||||
if (methods.auth) {
|
||||
instance.auth = methods.auth;
|
||||
}
|
||||
instance.modelFor = () => clusterModel;
|
||||
instance.router = { transitionTo: () => {} };
|
||||
return instance;
|
||||
}
|
||||
|
||||
const INIT_TESTS = [
|
||||
{
|
||||
clusterState: { needsInit: true },
|
||||
expected: INIT,
|
||||
description: 'forwards to INIT when cluster needs init',
|
||||
},
|
||||
{
|
||||
clusterState: { needsInit: false, sealed: true },
|
||||
expected: UNSEAL,
|
||||
description: 'forwards to UNSEAL if sealed and initialized',
|
||||
},
|
||||
{
|
||||
clusterState: { needsInit: false, sealed: false },
|
||||
expected: AUTH,
|
||||
description: 'forwards to AUTH if unsealed and initialized',
|
||||
},
|
||||
{
|
||||
clusterState: { dr: { isSecondary: true } },
|
||||
expected: DR_REPLICATION_SECONDARY,
|
||||
description: 'forwards to DR_REPLICATION_SECONDARY if is a dr secondary',
|
||||
},
|
||||
];
|
||||
|
||||
for (const { clusterState, expected, description } of INIT_TESTS) {
|
||||
test(`#targetRouteName init case: ${expected}`, function (assert) {
|
||||
const subject = createClusterRoute(this, clusterState);
|
||||
subject.routeName = CLUSTER;
|
||||
assert.strictEqual(subject.targetRouteName(), expected, description);
|
||||
});
|
||||
}
|
||||
|
||||
test('#targetRouteName happy path when not authed forwards to AUTH', function (assert) {
|
||||
const subject = createClusterRoute(
|
||||
this,
|
||||
{ needsInit: false, sealed: false, dr: { isSecondary: false } },
|
||||
{ auth: { currentToken: null } }
|
||||
);
|
||||
subject.router.currentRouteName = INIT;
|
||||
assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when inited and navigating to INIT');
|
||||
|
||||
subject.router.currentRouteName = UNSEAL;
|
||||
assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when unsealed and navigating to UNSEAL');
|
||||
|
||||
subject.router.currentRouteName = AUTH;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
AUTH,
|
||||
'forwards when non-authenticated and navigating to AUTH'
|
||||
);
|
||||
|
||||
subject.router.currentRouteName = DR_REPLICATION_SECONDARY;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
AUTH,
|
||||
'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
|
||||
);
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute', function (assert) {
|
||||
const redirectRouteURL = '/vault/secrets-engines/secret/create';
|
||||
const subject = createClusterRoute(this, { needsInit: false, sealed: false });
|
||||
subject.router.currentURL = redirectRouteURL;
|
||||
const spy = sinon.stub(subject.router, 'transitionTo');
|
||||
subject.transitionToTargetRoute();
|
||||
assert.ok(
|
||||
spy.calledWithExactly(AUTH, { queryParams: { redirect_to: redirectRouteURL } }),
|
||||
'calls transitionTo with the expected args'
|
||||
);
|
||||
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute with auth as a target', function (assert) {
|
||||
const subject = createClusterRoute(this, { needsInit: false, sealed: false });
|
||||
const spy = sinon.stub(subject.router, 'transitionTo');
|
||||
// in this case it's already transitioning to the AUTH route so we don't need to call transitionTo again
|
||||
subject.transitionToTargetRoute({ targetName: AUTH });
|
||||
assert.ok(spy.notCalled, 'transitionTo is not called');
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute with auth target, coming from cluster route', function (assert) {
|
||||
const subject = createClusterRoute(this, { needsInit: false, sealed: false });
|
||||
const spy = sinon.stub(subject.router, 'transitionTo');
|
||||
subject.transitionToTargetRoute({ targetName: CLUSTER_INDEX });
|
||||
assert.ok(spy.calledWithExactly(AUTH), 'calls transitionTo without redirect_to');
|
||||
spy.restore();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import EmberObject from '@ember/object';
|
||||
import ClusterRouteMixin from 'vault/mixins/cluster-route';
|
||||
import {
|
||||
INIT,
|
||||
UNSEAL,
|
||||
AUTH,
|
||||
CLUSTER,
|
||||
CLUSTER_INDEX,
|
||||
DR_REPLICATION_SECONDARY,
|
||||
REDIRECT,
|
||||
} from 'vault/lib/route-paths';
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Unit | Mixin | cluster route', function () {
|
||||
function createClusterRoute(
|
||||
clusterModel = {},
|
||||
methods = {
|
||||
router: { transitionTo: () => {} },
|
||||
hasKeyData: () => false,
|
||||
authToken: () => null,
|
||||
transitionTo: () => {},
|
||||
}
|
||||
) {
|
||||
const ClusterRouteObject = EmberObject.extend(
|
||||
ClusterRouteMixin,
|
||||
Object.assign(methods, { clusterModel: () => clusterModel })
|
||||
);
|
||||
return ClusterRouteObject.create();
|
||||
}
|
||||
|
||||
test('#targetRouteName init', function (assert) {
|
||||
let subject = createClusterRoute({ needsInit: true });
|
||||
subject.routeName = CLUSTER;
|
||||
assert.strictEqual(subject.targetRouteName(), INIT, 'forwards to INIT when cluster needs init');
|
||||
|
||||
subject = createClusterRoute({ needsInit: false, sealed: true });
|
||||
subject.routeName = CLUSTER;
|
||||
assert.strictEqual(subject.targetRouteName(), UNSEAL, 'forwards to UNSEAL if sealed and initialized');
|
||||
|
||||
subject = createClusterRoute({ needsInit: false });
|
||||
subject.routeName = CLUSTER;
|
||||
assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards to AUTH if unsealed and initialized');
|
||||
|
||||
subject = createClusterRoute({ dr: { isSecondary: true } });
|
||||
subject.routeName = CLUSTER;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
DR_REPLICATION_SECONDARY,
|
||||
'forwards to DR_REPLICATION_SECONDARY if is a dr secondary'
|
||||
);
|
||||
});
|
||||
|
||||
test('#targetRouteName when #hasDataKey is true', function (assert) {
|
||||
let subject = createClusterRoute(
|
||||
{ needsInit: false, sealed: true },
|
||||
{ hasKeyData: () => true, authToken: () => null }
|
||||
);
|
||||
|
||||
subject.routeName = CLUSTER;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
INIT,
|
||||
'still land on INIT if there are keys on the controller'
|
||||
);
|
||||
|
||||
subject.routeName = UNSEAL;
|
||||
assert.strictEqual(subject.targetRouteName(), UNSEAL, 'allowed to proceed to unseal');
|
||||
|
||||
subject = createClusterRoute(
|
||||
{ needsInit: false, sealed: false },
|
||||
{ hasKeyData: () => true, authToken: () => null }
|
||||
);
|
||||
|
||||
subject.routeName = AUTH;
|
||||
assert.strictEqual(subject.targetRouteName(), AUTH, 'allowed to proceed to auth');
|
||||
});
|
||||
|
||||
test('#targetRouteName happy path forwards to CLUSTER route', function (assert) {
|
||||
const subject = createClusterRoute(
|
||||
{ needsInit: false, sealed: false, dr: { isSecondary: false } },
|
||||
{ hasKeyData: () => false, authToken: () => 'a token' }
|
||||
);
|
||||
subject.routeName = INIT;
|
||||
assert.strictEqual(subject.targetRouteName(), CLUSTER, 'forwards when inited and navigating to INIT');
|
||||
|
||||
subject.routeName = UNSEAL;
|
||||
assert.strictEqual(subject.targetRouteName(), CLUSTER, 'forwards when unsealed and navigating to UNSEAL');
|
||||
|
||||
subject.routeName = AUTH;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
REDIRECT,
|
||||
'forwards when authenticated and navigating to AUTH'
|
||||
);
|
||||
|
||||
subject.routeName = DR_REPLICATION_SECONDARY;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
CLUSTER,
|
||||
'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
|
||||
);
|
||||
});
|
||||
|
||||
test('#targetRouteName happy path when not authed forwards to AUTH', function (assert) {
|
||||
const subject = createClusterRoute(
|
||||
{ needsInit: false, sealed: false, dr: { isSecondary: false } },
|
||||
{ hasKeyData: () => false, authToken: () => null }
|
||||
);
|
||||
subject.routeName = INIT;
|
||||
assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when inited and navigating to INIT');
|
||||
|
||||
subject.routeName = UNSEAL;
|
||||
assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when unsealed and navigating to UNSEAL');
|
||||
|
||||
subject.routeName = AUTH;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
AUTH,
|
||||
'forwards when non-authenticated and navigating to AUTH'
|
||||
);
|
||||
|
||||
subject.routeName = DR_REPLICATION_SECONDARY;
|
||||
assert.strictEqual(
|
||||
subject.targetRouteName(),
|
||||
AUTH,
|
||||
'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
|
||||
);
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute', function (assert) {
|
||||
const redirectRouteURL = '/vault/secrets-engines/secret/create';
|
||||
const subject = createClusterRoute({ needsInit: false, sealed: false });
|
||||
subject.router.currentURL = redirectRouteURL;
|
||||
const spy = sinon.spy(subject.router, 'transitionTo');
|
||||
subject.transitionToTargetRoute();
|
||||
assert.ok(
|
||||
spy.calledWithExactly(AUTH, { queryParams: { redirect_to: redirectRouteURL } }),
|
||||
'calls transitionTo with the expected args'
|
||||
);
|
||||
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute with auth as a target', function (assert) {
|
||||
const subject = createClusterRoute({ needsInit: false, sealed: false });
|
||||
const spy = sinon.spy(subject, 'transitionTo');
|
||||
// in this case it's already transitioning to the AUTH route so we don't need to call transitionTo again
|
||||
subject.transitionToTargetRoute({ targetName: AUTH });
|
||||
assert.ok(spy.notCalled, 'transitionTo is not called');
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute with auth target, coming from cluster route', function (assert) {
|
||||
const subject = createClusterRoute({ needsInit: false, sealed: false });
|
||||
const spy = sinon.spy(subject.router, 'transitionTo');
|
||||
subject.transitionToTargetRoute({ targetName: CLUSTER_INDEX });
|
||||
assert.ok(spy.calledWithExactly(AUTH), 'calls transitionTo without redirect_to');
|
||||
spy.restore();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue