UI: Upgrade to Ember 4.12 (#22122)

This commit is contained in:
Chelsea Shaw
2023-08-01 14:02:21 -05:00
committed by GitHub
parent 1d01045e85
commit 8731cee07a
115 changed files with 3294 additions and 1522 deletions

3
changelog/22122.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: upgrade Ember to 4.12
```

View File

@@ -9,8 +9,8 @@
"output-path": "../http/web_ui",
/**
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
*/
"isTypeScriptProject": false
}

View File

@@ -8,13 +8,14 @@
'use strict';
module.exports = {
parser: 'babel-eslint',
parser: '@babel/eslint-parser',
root: true,
parserOptions: {
ecmaVersion: 2018,
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
legacyDecorators: true,
requireConfigFile: false,
babelOptions: {
plugins: [['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }]],
},
},
plugins: ['ember'],
@@ -45,6 +46,7 @@ module.exports = {
files: [
'./.eslintrc.js',
'./.prettierrc.js',
'./.stylelintrc.js',
'./.template-lintrc.js',
'./ember-cli-build.js',
'./testem.js',
@@ -60,13 +62,7 @@ module.exports = {
browser: false,
node: true,
},
plugins: ['node'],
extends: ['plugin:node/recommended'],
rules: {
// this can be removed once the following is fixed
// https://github.com/mysticatea/eslint-plugin-node/issues/77
'node/no-unpublished-require': 'off',
},
extends: ['plugin:n/recommended'],
},
{
// test files

View File

@@ -17,5 +17,11 @@ module.exports = {
printWidth: 125,
},
},
{
files: '*.{js,ts}',
options: {
singleQuote: true,
},
},
],
};

8
ui/.stylelintignore Normal file
View File

@@ -0,0 +1,8 @@
# unconventional files
/blueprints/*/files/
# compiled output
/dist/
# addons
/.node_modules.ember-try/

5
ui/.stylelintrc.js Normal file
View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
};

View File

@@ -46,6 +46,7 @@ module.exports = {
allow: ['supported-auth-backends'],
},
'require-input-label': 'off',
'no-array-prototype-extensions': 'off',
},
ignore: ['lib/story-md', 'tests/**'],
// ember language server vscode extension does not currently respect the ignore field

View File

@@ -219,6 +219,8 @@ export default Component.extend(DEFAULTS, {
};
})
);
// without unloading the records there will be an issue where all methods set to list when unauthenticated will appear for all namespaces
// if possible, it would be more reliable to add a namespace attr to the model so we could filter against the current namespace rather than unloading all
next(() => {
store.unloadAll('auth-method');
});
@@ -265,7 +267,7 @@ export default Component.extend(DEFAULTS, {
return;
}
let response = null;
this.setOktaNumberChallenge(true);
this.args.setOktaNumberChallenge(true);
this.setCancellingAuth(false);
// keep polling /auth/okta/verify/:nonce API every 1s until a response is given with the correct number for the Okta Number Challenge
while (response === null) {
@@ -328,7 +330,7 @@ export default Component.extend(DEFAULTS, {
});
},
returnToLoginFromOktaNumberChallenge() {
this.setOktaNumberChallenge(false);
this.args.setOktaNumberChallenge(false);
this.set('oktaNumberChallengeAnswer', null);
},
},

View File

@@ -62,7 +62,7 @@ export default class DatabaseRoleEdit extends Component {
delete() {
const secret = this.args.model;
const backend = secret.backend;
secret
return secret
.destroyRecord()
.then(() => {
try {
@@ -89,7 +89,7 @@ export default class DatabaseRoleEdit extends Component {
const path = roleSecret.type === 'static' ? 'static-roles' : 'roles';
roleSecret.set('path', path);
}
roleSecret
return roleSecret
.save()
.then(() => {
try {
@@ -110,7 +110,7 @@ export default class DatabaseRoleEdit extends Component {
rotateRoleCred(id) {
const backend = this.args.model?.backend;
const adapter = this.store.adapterFor('database/credential');
adapter
return adapter
.rotateRoleCredentials(backend, id)
.then(() => {
this.flashMessages.success(`Success: Credentials for ${id} role were rotated`);

View File

@@ -55,7 +55,9 @@ export default Component.extend({
},
willDestroy() {
if (!this.model.isDestroyed && !this.model.isDestroying) {
// components are torn down after store is unloaded and will cause an error if attempt to unload record
const noTeardown = this.store && !this.store.isDestroying;
if (noTeardown && !this.model.isDestroyed && !this.model.isDestroying) {
this.model.unloadRecord();
}
this._super(...arguments);

View File

@@ -12,6 +12,7 @@ import { waitFor } from '@ember/test-waiters';
export default Component.extend({
flashMessages: service(),
store: service(),
'data-test-component': 'identity-edit-form',
attributeBindings: ['data-test-component'],
model: null,
@@ -73,12 +74,13 @@ export default Component.extend({
).drop(),
willDestroy() {
this._super(...arguments);
// components are torn down after store is disconnected and will cause an error if attempt to unload record
const noTeardown = this.store && !this.store.isDestroying;
const model = this.model;
if (!model) return;
if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) {
if (noTeardown && model && model.get('isDirty') && !model.isDestroyed && !model.isDestroying) {
model.rollbackAttributes();
}
this._super(...arguments);
},
actions: {

View File

@@ -6,6 +6,7 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { A } from '@ember/array';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import handleHasManySelection from 'core/utils/search-select-has-many';
@@ -40,14 +41,14 @@ export default class MfaLoginEnforcementForm extends Component {
searchSelectOptions = null;
@tracked name;
@tracked targets = [];
@tracked targets = A([]);
@tracked selectedTargetType = 'accessor';
@tracked selectedTargetValue = null;
@tracked searchSelect = {
options: [],
selected: [],
};
@tracked authMethods = [];
@tracked authMethods = A([]);
@tracked modelErrors;
constructor() {
@@ -100,6 +101,12 @@ export default class MfaLoginEnforcementForm extends Component {
return this.args.modelErrors || this.modelErrors;
}
updateModelForKey(key) {
const newValue = this.targets.filter((t) => t.key === key).map((t) => t.value);
// Set the model value directly instead of using Array methods (like .addObject)
this.args.model[key] = newValue;
}
@task
*save() {
this.modelErrors = {};
@@ -139,21 +146,22 @@ export default class MfaLoginEnforcementForm extends Component {
this.selectedTargetValue = selected;
}
}
@action
addTarget() {
const { label, key } = this.selectedTarget;
const value = this.selectedTargetValue;
this.targets.addObject({ label, value, key });
// add target to appropriate model property
this.args.model[key].addObject(value);
// recalculate value for appropriate model property
this.updateModelForKey(key);
this.selectedTargetValue = null;
this.resetTargetState();
}
@action
removeTarget(target) {
this.targets.removeObject(target);
// remove target from appropriate model property
this.args.model[target.key].removeObject(target.value);
// recalculate value for appropriate model property
this.updateModelForKey(target.key);
}
@action
cancel() {

View File

@@ -35,12 +35,12 @@ export default class MountBackendForm extends Component {
@tracked errorMessage = '';
willDestroy() {
// if unsaved, we want to unload so it doesn't show up in the auth mount list
super.willDestroy(...arguments);
if (this.args.mountModel) {
const method = this.args.mountModel.isNew ? 'unloadRecord' : 'rollbackAttributes';
this.args.mountModel[method]();
// components are torn down after store is unloaded and will cause an error if attempt to unload record
const noTeardown = this.store && !this.store.isDestroying;
if (noTeardown && this.args?.mountModel) {
this.args.mountModel.rollbackAttributes();
}
super.willDestroy(...arguments);
}
checkPathChange(type) {

View File

@@ -26,10 +26,10 @@ export default Component.extend(FocusOnInsertMixin, {
requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
willDestroyElement() {
this._super(...arguments);
if (this.model && this.model.isError) {
if (this.model && this.model.isError && !this.model.isDestroyed && !this.model.isDestroying) {
this.model.rollbackAttributes();
}
this._super(...arguments);
},
waitForKeyUp: task(function* () {

View File

@@ -35,14 +35,13 @@ export default class SecretEdit extends Component {
@service store;
@tracked secretData = null;
@tracked isV2 = false;
@tracked codemirrorString = null;
// fired on did-insert from render modifier
@action
createKvData(elem, [model]) {
if (!model.secretData && model.selectedVersion) {
this.isV2 = true;
if (this.isV2) {
// pre-fill secret data from selected version
model.secretData = model.belongsTo('selectedVersion').value().secretData;
}
this.secretData = KVObject.create({ content: [] }).fromJSON(model.secretData);
@@ -97,6 +96,9 @@ export default class SecretEdit extends Component {
@or('model.isLoading', 'model.isReloading', 'model.isSaving') requestInFlight;
@or('requestInFlight', 'model.isFolder', 'model.flagsIsInvalid') buttonDisabled;
get isV2() {
return !!this.args.model?.selectedVersion;
}
get modelForData() {
const { model } = this.args;
if (!model) return null;

View File

@@ -44,10 +44,10 @@ export default Component.extend(FocusOnInsertMixin, {
},
willDestroyElement() {
this._super(...arguments);
if (this.model && this.model.isError) {
if (this.model && this.model.isError && !this.model.isDestroyed && !this.model.isDestroying) {
this.model.rollbackAttributes();
}
this._super(...arguments);
},
transitionToRoute() {

View File

@@ -26,10 +26,10 @@ export default Component.extend(FocusOnInsertMixin, {
requestInFlight: or('key.isLoading', 'key.isReloading', 'key.isSaving'),
willDestroyElement() {
this._super(...arguments);
if (this.key && this.key.isError) {
if (this.key && this.key.isError && !this.key.isDestroyed && !this.key.isDestroying) {
this.key.rollbackAttributes();
}
this._super(...arguments);
},
waitForKeyUp: task(function* () {

View File

@@ -10,10 +10,10 @@ export function initialize() {
registerDeprecationHandler((message, options, next) => {
// filter deprecations that are scheduled to be removed in a specific version
// when upgrading or addressing deprecation warnings be sure to update this or remove if not needed
if (options?.until !== '5.0.0') {
next(message, options);
if (options?.until.includes('5.0')) {
return;
}
return;
next(message, options);
});
}

View File

@@ -6,6 +6,7 @@
export const INIT = 'vault.cluster.init';
export const UNSEAL = 'vault.cluster.unseal';
export const AUTH = 'vault.cluster.auth';
export const LOGOUT = 'vault.cluster.logout';
export const REDIRECT = 'vault.cluster.redirect';
export const CLUSTER = 'vault.cluster';
export const CLUSTER_INDEX = 'vault.cluster.index';

View File

@@ -7,6 +7,23 @@ import { computed } from '@ember/object';
import ObjectProxy from '@ember/object/proxy';
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
import { resolve } from 'rsvp';
/**
* after upgrading to Ember 4.12 a secrets test was erroring with "Cannot create a new tag for `<model::capabilities:undefined>` after it has been destroyed"
* see this GH issue for information on the fix https://github.com/emberjs/ember.js/issues/16541#issuecomment-382403523
*/
ObjectProxy.reopen({
unknownProperty(key) {
if (this.isDestroying || this.isDestroyed) {
return;
}
if (this.content && (this.content.isDestroying || this.content.isDestroyed)) {
return;
}
return this._super(key);
},
});
export function maybeQueryRecord(modelName, options = {}, ...keys) {
return computed(...keys, 'store', {

View File

@@ -56,6 +56,11 @@ export default Mixin.create({
);
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);
}

View File

@@ -19,7 +19,6 @@ export default Mixin.create({
return;
}
removeRecord(this.store, model);
model.destroy();
// it's important to unset the model on the controller since controllers are singletons
this.controller.set(modelPath, null);
},

View File

@@ -4,6 +4,7 @@
*/
import Mixin from '@ember/object/mixin';
import Ember from 'ember';
// this mixin relies on `unload-model-route` also being used
export default Mixin.create({
@@ -15,6 +16,7 @@ export default Mixin.create({
}
if (model.hasDirtyAttributes) {
if (
Ember.testing ||
window.confirm(
'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
)

View File

@@ -5,75 +5,104 @@
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import { inject as service } from '@ember/service';
import { alias, and, equal, gte, not, or } from '@ember/object/computed';
import { get, computed } from '@ember/object';
import { get } from '@ember/object';
export default Model.extend({
version: service(),
export default class ClusterModel extends Model {
@service version;
nodes: hasMany('nodes', { async: false }),
name: attr('string'),
status: attr('string'),
standby: attr('boolean'),
type: attr('string'),
license: attr('object'),
@hasMany('nodes', { async: false, inverse: null }) nodes;
@attr('string') name;
@attr('string') status;
@attr('boolean') standby;
@attr('string') type;
@attr('object') license;
/* Licensing concerns */
licenseExpiry: alias('license.expiry_time'),
licenseState: alias('license.state'),
get licenseExpiry() {
return this.license?.expiry_time;
}
get licenseState() {
return this.license?.state;
}
needsInit: computed('nodes', 'nodes.@each.initialized', function () {
// needs init if no nodes are initialized
return this.nodes.isEvery('initialized', false);
}),
get needsInit() {
return this.nodes.every((node) => {
return node.initialized === false;
});
}
unsealed: computed('nodes', 'nodes.{[],@each.sealed}', function () {
// unsealed if there's at least one unsealed node
return !!this.nodes.findBy('sealed', false);
}),
get unsealed() {
return !!this.nodes.find((node) => {
return node.sealed === false;
});
}
sealed: not('unsealed'),
get sealed() {
return !this.unsealed;
}
leaderNode: computed('nodes', 'nodes.[]', function () {
get leaderNode() {
const nodes = this.nodes;
if (nodes.get('length') === 1) {
return nodes.get('firstObject');
if (nodes.length === 1) {
return nodes[0];
} else {
return nodes.findBy('isLeader');
return nodes.find((node) => node.isLeader === true);
}
}),
}
sealThreshold: alias('leaderNode.sealThreshold'),
sealProgress: alias('leaderNode.progress'),
sealType: alias('leaderNode.type'),
storageType: alias('leaderNode.storageType'),
hcpLinkStatus: alias('leaderNode.hcpLinkStatus'),
hasProgress: gte('sealProgress', 1),
usingRaft: equal('storageType', 'raft'),
get sealThreshold() {
return this.leaderNode?.sealThreshold;
}
get sealProgress() {
return this.leaderNode?.progress;
}
get sealType() {
return this.leaderNode?.type;
}
get storageType() {
return this.leaderNode?.storageType;
}
get hcpLinkStatus() {
return this.leaderNode?.hcpLinkStatus;
}
get hasProgress() {
return this.sealProgress >= 1;
}
get usingRaft() {
return this.storageType === 'raft';
}
//replication mode - will only ever be 'unsupported'
//otherwise the particular mode will have the relevant mode attr through replication-attributes
mode: attr('string'),
allReplicationDisabled: and('{dr,performance}.replicationDisabled'),
anyReplicationEnabled: or('{dr,performance}.replicationEnabled'),
@attr('string') mode;
get allReplicationDisabled() {
return this.dr?.replicationDisabled && this.performance?.replicationDisabled;
}
get anyReplicationEnabled() {
return this.dr?.replicationEnabled || this.performance?.replicationEnabled;
}
dr: belongsTo('replication-attributes', { async: false, inverse: null }),
performance: belongsTo('replication-attributes', { async: false, inverse: null }),
@belongsTo('replication-attributes', { async: false, inverse: null }) dr;
@belongsTo('replication-attributes', { async: false, inverse: null }) performance;
// this service exposes what mode the UI is currently viewing
// replicationAttrs will then return the relevant `replication-attributes` model
rm: service('replication-mode'),
drMode: alias('dr.mode'),
replicationMode: alias('rm.mode'),
replicationModeForDisplay: computed('replicationMode', function () {
@service('replication-mode') rm;
get drMode() {
return this.dr.mode;
}
get replicationMode() {
return this.rm.mode;
}
get replicationModeForDisplay() {
return this.replicationMode === 'dr' ? 'Disaster Recovery' : 'Performance';
}),
replicationIsInitializing: computed('dr.mode', 'performance.mode', function () {
}
get replicationIsInitializing() {
// a mode of null only happens when a cluster is being initialized
// otherwise the mode will be 'disabled', 'primary', 'secondary'
return !this.dr.mode || !this.performance.mode;
}),
replicationAttrs: computed('dr.mode', 'performance.mode', 'replicationMode', function () {
return !this.dr?.mode || !this.performance?.mode;
}
get replicationAttrs() {
const replicationMode = this.replicationMode;
return replicationMode ? get(this, replicationMode) : null;
}),
});
}
}

View File

@@ -52,12 +52,6 @@ export default class KubernetesRoleModel extends Model {
})
kubernetesRoleName;
@attr('string', {
label: 'Service account name',
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
})
serviceAccountName;
@attr('string', {
label: 'Allowed Kubernetes namespaces',
subText:

View File

@@ -160,13 +160,6 @@ export default class SecretEngineModel extends Model {
return 'vault.cluster.secrets.backend.list-root';
}
get accessor() {
if (this.version === 2) {
return `v2 ${this.accessor}`;
}
return this.accessor;
}
get localDisplay() {
return this.local ? 'local' : 'replicated';
}

View File

@@ -5,6 +5,7 @@
import AdapterError from '@ember-data/adapter/error';
import { set } from '@ember/object';
import Ember from 'ember';
import { resolve } from 'rsvp';
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
@@ -314,6 +315,12 @@ export default Route.extend(UnloadModelRoute, {
willTransition(transition) {
/* eslint-disable-next-line ember/no-controller-access-in-routes */
const { mode, model } = this.controller;
// If model is clean or deleted, continue
if (!model.hasDirtyAttributes || model.isDeleted) {
return true;
}
// TODO: below is KV v2 logic, remove with engine work
const version = model.get('selectedVersion');
const changed = model.changedAttributes();
const changedKeys = Object.keys(changed);
@@ -330,9 +337,10 @@ export default Route.extend(UnloadModelRoute, {
// and explicity ignore it here
if (
(mode !== 'show' && changedKeys.length && changedKeys[0] !== 'backend') ||
(mode !== 'show' && version && Object.keys(version.changedAttributes()).length)
(mode !== 'show' && version && version.hasDirtyAttributes)
) {
if (
Ember.testing ||
window.confirm(
'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
)

View File

@@ -9,11 +9,6 @@ import { inject as service } from '@ember/service';
export default class VaultClusterSettingsAuthEnableRoute extends Route {
@service store;
beforeModel() {
// Unload to prevent naming collisions when we mount a new engine
this.store.unloadAll('auth-method');
}
model() {
const authMethod = this.store.createRecord('auth-method');
authMethod.set('config', this.store.createRecord('mount-config'));

View File

@@ -9,11 +9,6 @@ import { inject as service } from '@ember/service';
export default class VaultClusterSettingsMountSecretBackendRoute extends Route {
@service store;
beforeModel() {
// Unload to prevent naming collisions when we mount a new engine
this.store.unloadAll('secret-engine');
}
model() {
const secretEngine = this.store.createRecord('secret-engine');
secretEngine.set('config', this.store.createRecord('mount-config'));

View File

@@ -40,6 +40,7 @@ export default class MfaLoginEnforcementSerializer extends ApplicationSerializer
// ensure that they are sent to the server, otherwise removing items will not be persisted
json.auth_method_accessors = json.auth_method_accessors || [];
json.auth_method_types = json.auth_method_types || [];
// TODO: create array transform which serializes an empty array if empty
return this.transformHasManyKeys(json, 'server');
}
}

View File

@@ -1,7 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend();

View File

@@ -4,19 +4,21 @@
*/
import Ember from 'ember';
import { resolve, reject } from 'rsvp';
import { assign } from '@ember/polyfills';
import { task, timeout } from 'ember-concurrency';
import { getOwner } from '@ember/application';
import { isArray } from '@ember/array';
import { computed, get } from '@ember/object';
import { capitalize } from '@ember/string';
import fetch from 'fetch';
import { getOwner } from '@ember/application';
import { alias } from '@ember/object/computed';
import { assign } from '@ember/polyfills';
import Service, { inject as service } from '@ember/service';
import getStorage from '../lib/token-storage';
import { capitalize } from '@ember/string';
import fetch from 'fetch';
import { resolve, reject } from 'rsvp';
import getStorage from 'vault/lib/token-storage';
import ENV from 'vault/config/environment';
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
import { task, timeout } from 'ember-concurrency';
const TOKEN_SEPARATOR = '☃';
const TOKEN_PREFIX = 'vault-';
const ROOT_PREFIX = '_root_';
@@ -26,7 +28,7 @@ export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
export default Service.extend({
permissions: service(),
store: service(),
currentCluster: service(),
router: service(),
namespaceService: service('namespace'),
@@ -40,9 +42,7 @@ export default Service.extend({
return expiration ? this.now() >= expiration : null;
},
get activeCluster() {
return this.activeClusterId ? this.store.peekRecord('cluster', this.activeClusterId) : null;
},
activeCluster: alias('currentCluster.cluster'),
// eslint-disable-next-line
tokens: computed({

View File

@@ -4,11 +4,12 @@
*/
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default Service.extend({
cluster: null,
export default class CurrentClusterService extends Service {
@tracked cluster = null;
setCluster(cluster) {
this.set('cluster', cluster);
},
});
this.cluster = cluster;
}
}

View File

@@ -4,15 +4,16 @@
*/
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default Service.extend({
mode: null,
export default class ReplicationModeService extends Service {
@tracked mode = null;
getMode() {
return this.mode;
},
}
setMode(mode) {
this.set('mode', mode);
},
});
this.mode = mode;
}
}

View File

@@ -140,7 +140,7 @@ export default class StoreService extends Store {
// pushes records into the store and returns the result
fetchPage(modelName, query) {
const response = this.constructResponse(modelName, query);
this.peekAll(modelName).map((model) => model.unloadRecord());
this.unloadAll(modelName);
return new Promise((resolve) => {
// after the above unloadRecords are finished, push into store
schedule('destroy', () => {
@@ -187,4 +187,29 @@ export default class StoreService extends Store {
clearAllDatasets() {
this.clearDataset();
}
/**
* this is designed to be a temporary workaround to an issue in the test environment after upgrading to Ember 4.12
* when performing an unloadAll or unloadRecord for auth-method or secret-engine models within the app code an error breaks the tests
* after the test run is finished during teardown an unloadAll happens and the error "Expected a stable identifier" is thrown
* it seems that when the unload happens in the app, for some reason the mount-config relationship models are not unloaded
* then when the unloadAll happens a second time during test teardown there seems to be an issue since those records should already have been unloaded
* when logging in the teardownRecord hook, it appears that other embedded inverse: null relationships such as replication-attributes are torn down when the parent model is unloaded
* the following fixes the issue by explicitly unloading the mount-config models associated to the parent
* this should be looked into further to find the root cause, at which time these overrides may be removed
*/
unloadAll(modelName) {
const hasMountConfig = ['auth-method', 'secret-engine'];
if (hasMountConfig.includes(modelName)) {
this.peekAll(modelName).forEach((record) => this.unloadRecord(record));
} else {
super.unloadAll(modelName);
}
}
unloadRecord(record) {
const hasMountConfig = ['auth-method', 'secret-engine'];
if (record && hasMountConfig.includes(record.constructor.modelName) && record.config) {
super.unloadRecord(record.config);
}
super.unloadRecord(record);
}
}

View File

@@ -69,8 +69,8 @@
</div>
{{/if}}
<div class="legend-center">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
<span class="light-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "0.label")}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "1.label")}}</span>
</div>
{{else}}
<div class="chart-empty-state">

View File

@@ -15,11 +15,7 @@
{{! Component must be in curly bracket notation }}
{{! template-lint-disable no-curly-component-invocation }}
{{#modal-dialog
tagName="div"
tetherTarget=this.tooltipTarget
targetAttachment="bottom middle"
attachment="bottom middle"
offset="35px 0"
tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="35px 0"
}}
<div class={{concat "chart-tooltip horizontal-chart " (if this.isLabel "is-label-fit-content")}}>
<p>{{this.tooltipText}}</p>

View File

@@ -18,11 +18,7 @@
{{! Component must be in curly bracket notation }}
{{! template-lint-disable no-curly-component-invocation }}
{{#modal-dialog
tagName="div"
tetherTarget=this.tooltipTarget
targetAttachment="bottom middle"
attachment="bottom middle"
offset="35px 0"
tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="35px 0"
}}
<div class="chart-tooltip line-chart">
<p class="bold">{{this.tooltipMonth}}</p>

View File

@@ -40,8 +40,8 @@
{{#if @verticalBarChartData}}
<div data-test-monthly-usage-legend class="legend-right">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
<span class="light-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "0.label")}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "1.label")}}</span>
</div>
{{/if}}
</div>

View File

@@ -78,8 +78,8 @@
{{#if this.hasAverageNewClients}}
<div class="legend-right" data-test-running-total-legend>
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
<span class="light-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "0.label")}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "1.label")}}</span>
</div>
{{/if}}
</div>

View File

@@ -18,11 +18,7 @@
{{! Component must be in curly bracket notation }}
{{! template-lint-disable no-curly-component-invocation }}
{{#modal-dialog
tagName="div"
tetherTarget=this.tooltipTarget
targetAttachment="bottom middle"
attachment="bottom middle"
offset="10px 0"
tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="10px 0"
}}
<div class="chart-tooltip vertical-chart">
<p>{{this.tooltipTotal}}</p>

View File

@@ -153,7 +153,7 @@
{{/if}}
</div>
<div class="control">
<SecretLink @mode="list" class="button">
<SecretLink @mode="list" class="button" data-test-database-role-cancel>
Cancel
</SecretLink>
</div>

View File

@@ -1,6 +1,6 @@
<PopupMenu @name="alias-menu">
<Confirm as |c|>
{{#let this.params.firstObject as |item|}}
{{#let (get this.params "0") as |item|}}
<nav class="menu">
<ul class="menu-list">
<li class="action">

View File

@@ -11,10 +11,7 @@
<nav class="tabs has-bottom-margin-l">
<ul>
{{! template-lint-configure no-nested-interactive "warn" }}
<li
aria-selected={{if (eq @unwrapActiveTab "data") "true" "false"}}
class={{if (eq @unwrapActiveTab "data") "is-active"}}
>
<li class={{if (eq @unwrapActiveTab "data") "is-active"}}>
<button
type="button"
class="link link-plain tab has-text-weight-semibold"
@@ -24,10 +21,7 @@
Data
</button>
</li>
<li
aria-selected={{if (eq @unwrapActiveTab "data") "true" "false"}}
class={{if (eq @unwrapActiveTab "details") "is-active"}}
>
<li class={{if (eq @unwrapActiveTab "details") "is-active"}}>
<button
type="button"
class="link link-plain tab has-text-weight-semibold"

View File

@@ -3,9 +3,7 @@
</div>
{{#if this.featureComponent}}
{{#component
(if this.shouldRender this.tutorialComponent)
onAdvance=(action "advanceWizard")
onDismiss=(action "dismissWizard")
(if this.shouldRender this.tutorialComponent) onAdvance=(action "advanceWizard") onDismiss=(action "dismissWizard")
}}
{{component
this.featureComponent

View File

@@ -47,25 +47,29 @@
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each-in this.model.customMetadata as |key value|}}
<InfoTableRow @alwaysRender={{false}} @label={{key}} @value={{value}} />
{{else if this.noReadAccess}}
<EmptyState
@title="You do not have access to read secret metadata"
@bottomBorder={{true}}
@message="In order to edit secret metadata access, the UI requires read permissions; otherwise, data may be deleted. Edits can still be made via the API and CLI."
/>
{{else}}
<EmptyState
@title="No custom metadata"
@bottomBorder={{true}}
@message="This data is version-agnostic and is usually used to describe the secret being stored."
>
{{#if this.model.canUpdateMetadata}}
<LinkTo @route="vault.cluster.secrets.backend.edit-metadata" @model={{this.model.id}} data-test-add-custom-metadata>
Add metadata
</LinkTo>
{{/if}}
</EmptyState>
{{/each-in}}
{{else}}{{#if this.noReadAccess}}
<EmptyState
@title="You do not have access to read secret metadata"
@bottomBorder={{true}}
@message="In order to edit secret metadata access, the UI requires read permissions; otherwise, data may be deleted. Edits can still be made via the API and CLI."
/>
{{else}}
<EmptyState
@title="No custom metadata"
@bottomBorder={{true}}
@message="This data is version-agnostic and is usually used to describe the secret being stored."
>
{{#if this.model.canUpdateMetadata}}
<LinkTo
@route="vault.cluster.secrets.backend.edit-metadata"
@model={{this.model.id}}
data-test-add-custom-metadata
>
Add metadata
</LinkTo>
{{/if}}
</EmptyState>
{{/if}}{{/each-in}}
</div>
{{#unless this.noReadAccess}}
<div class="form-section">

View File

@@ -78,7 +78,7 @@
</div>
{{#if backend.accessor}}
<code class="has-text-grey is-size-8">
{{backend.accessor}}
{{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}}
</code>
{{/if}}
{{#if backend.description}}

View File

@@ -3,7 +3,7 @@
"packages": [
{
"name": "ember-cli",
"version": "4.4.0",
"version": "4.12.1",
"blueprints": [
{
"name": "app",

View File

@@ -94,18 +94,5 @@ module.exports = function (defaults) {
app.import('node_modules/@hashicorp/structure-icons/dist/loading.css');
app.import('node_modules/@hashicorp/structure-icons/dist/run.css');
// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
return app.toTree();
};

View File

@@ -66,11 +66,12 @@ export default Component.extend({
).drop(),
willDestroy() {
this._super(...arguments);
// components are torn down after store is unloaded and will cause an error if attempt to unload record
const noTeardown = this.store && !this.store.isDestroying;
const { model } = this;
if (!model) return;
if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) {
if (noTeardown && model && model.get('isDirty') && !model.isDestroyed && !model.isDestroying) {
model.rollbackAttributes();
}
this._super(...arguments);
},
});

View File

@@ -38,7 +38,7 @@
class="button copy-button is-compact"
data-test-copy-button
>
<Icon @name="clipboard-copy" aria-hidden="Copy value" />
<Icon @name="clipboard-copy" aria-label="Copy value" />
</CopyButton>
</div>
{{/if}}

View File

@@ -37,7 +37,7 @@
class="copy-button button {{if @displayOnly 'is-compact'}}"
data-test-copy-button
>
<Icon @name="clipboard-copy" aria-hidden="Copy value" />
<Icon @name="clipboard-copy" aria-label="Copy value" />
</CopyButton>
{{/if}}
{{#if @allowDownload}}

View File

@@ -1,5 +1,5 @@
<EmberWormhole @to="modal-wormhole">
<div class="{{this.modalClass}} {{if this.isActive 'is-active'}}" aria-modal="true" data-test-modal-div>
<div class="{{this.modalClass}} {{if this.isActive 'is-active'}}" data-test-modal-div>
<div class="modal-background" role="button" {{on "click" @onClose}} data-test-modal-background={{@title}}></div>
<div class="modal-card">
<header class="modal-card-head">

View File

@@ -1,7 +1,6 @@
<div class="navigate-filter">
<div class="field" data-test-nav-input>
<p class="control has-icons-left">
{{! template-lint-disable no-down-event-binding }}
<Input
id={{this.inputId}}
class="filter input"

View File

@@ -45,7 +45,6 @@ export default class TtlPickerComponent extends Component {
@tracked recalculateSeconds = false;
@tracked time = ''; // if defaultValue is NOT set, then do not display a defaultValue.
@tracked unit = 's';
@tracked recalculateSeconds = false;
@tracked errorMessage = '';
/* Used internally */

View File

@@ -18,58 +18,54 @@ import 'codemirror/mode/ruby/ruby';
import 'codemirror/mode/javascript/javascript';
export default class CodeMirrorModifier extends Modifier {
didInstall() {
this._setup();
}
didUpdateArguments() {
this._editor.setOption('readOnly', this.args.named.readOnly);
if (!this.args.named.content) {
return;
}
if (this._editor.getValue() !== this.args.named.content) {
this._editor.setValue(this.args.named.content);
modify(element, positionalArgs, namedArgs) {
// setup codemirror initially when modifier is installed on the element
if (!this._editor) {
this._setup(element, namedArgs);
} else {
// this hook also fires any time there is a change to tracked state
this._editor.setOption('readOnly', namedArgs.readOnly);
if (namedArgs.content && this._editor.getValue() !== namedArgs.content) {
this._editor.setValue(namedArgs.content);
}
}
}
@action
_onChange(editor) {
_onChange(namedArgs, editor) {
// avoid sending change event after initial setup when editor value is set to content
if (this.args.named.content !== editor.getValue()) {
this.args.named.onUpdate(editor.getValue(), this._editor);
if (namedArgs.content !== editor.getValue()) {
namedArgs.onUpdate(editor.getValue(), this._editor);
}
}
@action
_onFocus(editor) {
this.args.named.onFocus(editor.getValue());
_onFocus(namedArgs, editor) {
namedArgs.onFocus(editor.getValue());
}
_setup() {
if (!this.element) {
throw new Error('CodeMirror modifier has no element');
}
const editor = codemirror(this.element, {
_setup(element, namedArgs) {
const editor = codemirror(element, {
// IMPORTANT: `gutters` must come before `lint` since the presence of
// `gutters` is cached internally when `lint` is toggled
gutters: this.args.named.gutters || ['CodeMirror-lint-markers'],
gutters: namedArgs.gutters || ['CodeMirror-lint-markers'],
matchBrackets: true,
lint: { lintOnChange: true },
showCursorWhenSelecting: true,
styleActiveLine: true,
tabSize: 2,
// all values we can pass into the JsonEditor
extraKeys: this.args.named.extraKeys || '',
lineNumbers: this.args.named.lineNumbers,
mode: this.args.named.mode || 'application/json',
readOnly: this.args.named.readOnly || false,
theme: this.args.named.theme || 'hashi',
value: this.args.named.content || '',
viewportMargin: this.args.named.viewportMargin || '',
extraKeys: namedArgs.extraKeys || '',
lineNumbers: namedArgs.lineNumbers,
mode: namedArgs.mode || 'application/json',
readOnly: namedArgs.readOnly || false,
theme: namedArgs.theme || 'hashi',
value: namedArgs.content || '',
viewportMargin: namedArgs.viewportMargin || '',
});
editor.on('change', bind(this, this._onChange));
editor.on('focus', bind(this, this._onFocus));
editor.on('change', bind(this, this._onChange, namedArgs));
editor.on('focus', bind(this, this._onFocus, namedArgs));
this._editor = editor;
}

View File

@@ -108,7 +108,7 @@ export const namespaceArrayToObject = (totalClientsByNamespace, newClientsByName
const newNamespaceCounts = newClientsByNamespace?.find((n) => n.label === ns.label);
if (newNamespaceCounts) {
const { label, clients, entity_clients, non_entity_clients } = newNamespaceCounts;
const newClientsByMount = [...newNamespaceCounts?.mounts];
const newClientsByMount = [...newNamespaceCounts.mounts];
const nestNewClientsWithinMounts = ns.mounts?.map((mount) => {
const new_clients = newClientsByMount?.find((m) => m.label === mount.label) || {};
return {

View File

@@ -4,7 +4,7 @@
*/
/* eslint-env node */
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
'use strict';
var path = require('path');

View File

@@ -5,7 +5,7 @@
/* eslint-env node */
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
'use strict';
const EngineAddon = require('ember-engines/lib/engine-addon');

View File

@@ -4,7 +4,7 @@
*/
/* eslint-env node */
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
'use strict';
const { buildEngine } = require('ember-engines/lib/engine-addon');

View File

@@ -5,7 +5,7 @@
/* eslint-env node */
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
'use strict';
const EngineAddon = require('ember-engines/lib/engine-addon');

View File

@@ -20,7 +20,7 @@
</ToolbarActions>
</Toolbar>
{{#if (not (eq @cluster 403))}}
{{#if (not-eq @cluster 403)}}
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-xl has-border-bottom-light has-bottom-padding-s">
Cluster Config
</h2>
@@ -32,7 +32,7 @@
{{/each}}
{{/if}}
{{#if (not (eq @acme 403))}}
{{#if (not-eq @acme 403)}}
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-xl has-border-bottom-light has-bottom-padding-s">
ACME Config
</h2>
@@ -44,7 +44,7 @@
{{/each}}
{{/if}}
{{#if (not (eq @urls 403))}}
{{#if (not-eq @urls 403)}}
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-xl has-border-bottom-light has-bottom-padding-s">
Global URLs
</h2>
@@ -55,7 +55,7 @@
/>
{{/if}}
{{#if (not (eq @crl 403))}}
{{#if (not-eq @crl 403)}}
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-xl has-border-bottom-light has-bottom-padding-s">
Certificate Revocation List (CRL)
</h2>

View File

@@ -14,7 +14,7 @@
{{#if pkiIssuer.isDefault}}
<span class="tag has-text-grey-dark" data-test-is-default={{idx}}>default issuer</span>
{{/if}}
{{#if (not (eq pkiIssuer.isRoot undefined))}}
{{#if (not-eq pkiIssuer.isRoot undefined)}}
<span class="tag has-text-grey-dark" data-test-is-root-tag={{idx}}>{{if
pkiIssuer.isRoot
"root"

View File

@@ -25,7 +25,6 @@ export default class PkiKeyDetails extends Component<Args> {
this.flashMessages.success('Key deleted successfully.');
this.router.transitionTo('vault.cluster.secrets.backend.pki.keys.index');
} catch (error) {
this.args.key.rollbackAttributes();
this.flashMessages.danger(errorMessage(error));
}
}

View File

@@ -7,7 +7,7 @@
>
<h2 class="title-number">{{format-number (if (eq @issuers 404) 0 @issuers.length)}}</h2>
</OverviewCard>
{{#if (not (eq @roles 403))}}
{{#if (not-eq @roles 403)}}
<OverviewCard
@cardTitle="Roles"
@subText="The total number of roles in this PKI mount that have been created to generate certificates."

View File

@@ -75,7 +75,7 @@
</h2>
{{#if @urls.canSet}}
{{#each @urls.allFields as |attr|}}
{{#if (not (eq attr.name "mountPath"))}}
{{#if (not-eq attr.name "mountPath")}}
<FormField
@attr={{attr}}
@mode="create"

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: MPL-2.0
*/
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
const { buildEngine } = require('ember-engines/lib/engine-addon');
module.exports = buildEngine({

View File

@@ -123,10 +123,6 @@ export default Component.extend({
return ['cubbyhole', 'system', 'token', 'identity', 'ns_system', 'ns_identity', 'ns_token'];
}),
willDestroyElement() {
this._super(...arguments);
},
actions: {
async pathsChanged(paths) {
// set paths on the model

View File

@@ -5,7 +5,7 @@
/* eslint-env node */
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
'use strict';
const EngineAddon = require('ember-engines/lib/engine-addon');

View File

@@ -11,7 +11,9 @@
"scripts": {
"build": "ember build --environment=production && cp metadata.json ../http/web_ui/metadata.json",
"build:dev": "ember build",
"lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix",
"lint:css": "stylelint \"**/*.css\"",
"lint:css:fix": "yarn lint:css --fix",
"lint:fix": "npm-run-all --print-name --aggregate-output --continue-on-error --parallel \"lint:*:fix\"",
"lint:hbs": "ember-template-lint '**/*.hbs'",
"lint:hbs:quiet": "ember-template-lint '**/*.hbs' --quiet",
"lint:hbs:fix": "ember-template-lint . --fix",
@@ -25,7 +27,7 @@
"start": "VAULT_ADDR=http://localhost:8200; ember server --proxy=$VAULT_ADDR",
"start2": "ember server --proxy=http://localhost:8202 --port=4202",
"start:mirage": "start () { MIRAGE_DEV_HANDLER=$1 yarn run start; }; start",
"test": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/start-vault.js",
"test": "npm-run-all --print-name lint:js:quiet lint:hbs:quiet && node scripts/start-vault.js",
"test:enos": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/enos-test-ember.js",
"test:oss": "yarn run test -f='!enterprise'",
"test:quick": "node scripts/start-vault.js",
@@ -51,12 +53,15 @@
]
},
"devDependencies": {
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-block-scoping": "^7.12.1",
"@ember/legacy-built-in-components": "^0.4.1",
"@ember/optional-features": "^2.0.0",
"@ember/render-modifiers": "^1.0.2",
"@ember/test-helpers": "2.8.1",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "2.9.3",
"@ember/test-waiters": "^3.0.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
@@ -97,7 +102,6 @@
"@typescript-eslint/parser": "^5.19.0",
"asn1js": "^2.2.0",
"autosize": "^4.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-inline-json-import": "^0.3.2",
"base64-js": "^1.3.1",
"broccoli-asset-rev": "^3.0.0",
@@ -116,9 +120,9 @@
"deepmerge": "^4.0.0",
"doctoc": "^2.2.0",
"dompurify": "^3.0.2",
"ember-auto-import": "2.4.2",
"ember-auto-import": "2.6.3",
"ember-basic-dropdown": "6.0.1",
"ember-cli": "~4.4.0",
"ember-cli": "~4.12.1",
"ember-cli-autoprefixer": "^0.8.1",
"ember-cli-babel": "^7.26.11",
"ember-cli-clipboard": "0.16.0",
@@ -126,7 +130,7 @@
"ember-cli-dependency-checker": "^3.3.1",
"ember-cli-deprecation-workflow": "^2.1.0",
"ember-cli-flash": "4.0.0",
"ember-cli-htmlbars": "^6.0.1",
"ember-cli-htmlbars": "^6.2.0",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-mirage": "2.4.0",
"ember-cli-page-object": "1.17.10",
@@ -139,40 +143,39 @@
"ember-concurrency": "2.3.4",
"ember-copy": "2.0.1",
"ember-d3": "^0.5.1",
"ember-data": "~4.5.0",
"ember-data": "~4.11.3",
"ember-engines": "0.8.23",
"ember-export-application-global": "^2.0.1",
"ember-fetch": "^8.1.1",
"ember-fetch": "^8.1.2",
"ember-inflector": "4.0.2",
"ember-load-initializers": "^2.1.2",
"ember-maybe-in-element": "^2.0.3",
"ember-modal-dialog": "^4.0.1",
"ember-modifier": "^3.1.0",
"ember-modifier": "^4.1.0",
"ember-page-title": "^7.0.0",
"ember-power-select": "6.0.1",
"ember-qrcode-shim": "^0.4.0",
"ember-qunit": "6.0.0",
"ember-resolver": "^8.0.3",
"ember-resolver": "^10.0.0",
"ember-responsive": "5.0.0",
"ember-router-helpers": "^0.4.0",
"ember-service-worker": "meirish/ember-service-worker#configurable-scope",
"ember-sinon": "^4.0.0",
"ember-source": "4.4.4",
"ember-source": "~4.12.0",
"ember-svg-jar": "2.4.0",
"ember-template-lint": "4.8.0",
"ember-template-lint": "5.7.2",
"ember-template-lint-plugin-prettier": "4.0.0",
"ember-test-selectors": "6.0.0",
"ember-tether": "^2.0.1",
"ember-truth-helpers": "3.0.0",
"ember-wormhole": "0.6.0",
"escape-string-regexp": "^2.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-ember": "^10.6.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-qunit": "^7.2.0",
"eslint-plugin-ember": "^11.5.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-qunit": "^7.3.4",
"filesize": "^4.2.1",
"flat": "^4.1.0",
"jsondiffpatch": "^0.4.1",
@@ -183,21 +186,25 @@
"npm-run-all": "^4.1.5",
"pkijs": "^2.2.2",
"pretender": "^3.4.3",
"prettier": "2.6.2",
"prettier": "2.8.7",
"prettier-eslint-cli": "^7.1.0",
"pvutils": "^1.0.17",
"qunit": "^2.19.1",
"qunit": "^2.19.4",
"qunit-dom": "^2.0.0",
"sass": "^1.58.3",
"sass-svg-uri": "^1.0.0",
"shell-quote": "^1.6.1",
"string.prototype.endswith": "^0.2.0",
"string.prototype.startswith": "^0.2.0",
"stylelint": "^15.4.0",
"stylelint-config-standard": "^32.0.0",
"stylelint-prettier": "^3.0.0",
"swagger-ui-dist": "^3.36.2",
"text-encoder-lite": "2.0.0",
"tracked-built-ins": "^3.1.1",
"typescript": "^4.8.4",
"walk-sync": "^2.0.2",
"webpack": "5.73.0",
"webpack": "5.78.0",
"xstate": "^3.3.3"
},
"resolutions": {
@@ -216,7 +223,8 @@
"serialize-javascript": "^3.1.0",
"underscore": "^1.12.1",
"trim": "^0.0.3",
"xmlhttprequest-ssl": "^1.6.2"
"xmlhttprequest-ssl": "^1.6.2",
"@embroider/macros": "^1.0.0"
},
"engines": {
"node": "16"

View File

@@ -6,7 +6,7 @@
/* eslint-env node */
/* eslint-disable no-console */
/* eslint-disable no-process-exit */
/* eslint-disable node/no-extraneous-require */
/* eslint-disable n/no-extraneous-require */
var readline = require('readline');
const testHelper = require('./test-helper');

View File

@@ -9,7 +9,6 @@ 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 logout from 'vault/tests/pages/logout';
module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
setupApplicationTest(hooks);
@@ -19,10 +18,6 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
test('it navigates to namespaces page', async function (assert) {
assert.expect(1);
await page.visit();

View File

@@ -10,7 +10,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/auth/enable';
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
import { supportedManagedAuthBackends } from 'vault/helpers/supported-managed-auth-backends';
@@ -26,10 +25,6 @@ module('Acceptance | auth backend list', function (hooks) {
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
test('userpass secret backend', async function (assert) {
let n = Math.random();
const path1 = `userpass-${++n}`;

View File

@@ -13,7 +13,6 @@ import authForm from '../pages/components/auth-form';
import jwtForm from '../pages/components/auth-jwt';
import { create } from 'ember-cli-page-object';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
import logout from 'vault/tests/pages/logout';
const component = create(authForm);
const jwtComponent = create(jwtForm);
@@ -27,13 +26,11 @@ module('Acceptance | auth', function (hooks) {
shouldAdvanceTime: true,
});
this.server = apiStub({ usePassthrough: true });
return logout.visit();
});
hooks.afterEach(function () {
this.clock.restore();
this.server.shutdown();
return logout.visit();
});
test('auth query params', async function (assert) {

View File

@@ -9,7 +9,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
module('Acceptance | aws secret backend', function (hooks) {
@@ -20,20 +19,6 @@ module('Acceptance | aws secret backend', function (hooks) {
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
const POLICY = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: 'iam:*',
Resource: '*',
},
],
};
test('aws backend', async function (assert) {
assert.expect(12);
const path = `aws-${this.uid}`;
@@ -82,8 +67,6 @@ module('Acceptance | aws secret backend', function (hooks) {
await fillIn('[data-test-input="name"]', roleName);
findAll('.CodeMirror')[0].CodeMirror.setValue(JSON.stringify(POLICY));
// save the role
await click('[data-test-role-aws-create]');
await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this

View File

@@ -29,7 +29,6 @@ module('Acceptance | cluster', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
await logout.visit();
return authPage.login();
});
@@ -46,7 +45,6 @@ module('Acceptance | cluster', function (hooks) {
await visit('/vault/access');
assert.dom('[data-test-sidebar-nav-link="Policies"]').doesNotExist();
await logout.visit();
});
test('it hides mfa setup if user has not entityId (ex: is a root user)', async function (assert) {
@@ -83,6 +81,5 @@ module('Acceptance | cluster', function (hooks) {
await visit('/vault/access');
assert.dom('[data-test-sidebar-nav-link="Policies"]').hasAttribute('href', '/ui/vault/policies/rgp');
await logout.visit();
});
});

View File

@@ -7,7 +7,6 @@ import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, fillIn } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import { setupMirage } from 'ember-cli-mirage/test-support';
@@ -16,7 +15,6 @@ module('Acceptance | Enterprise | keymgmt', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(async function () {
await logout.visit();
return authPage.login();
});

View File

@@ -32,7 +32,6 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
await click('[data-test-namespace-toggle]');
assert.dom('[data-test-current-namespace]').hasText('root', 'root renders as current namespace');
assert.dom('[data-test-namespace-link]').doesNotExist('Additional namespace have been cleared');
await logout.visit();
});
test('it shows nested namespaces if you log in with a namspace starting with a /', async function (assert) {

View File

@@ -1,4 +1,9 @@
import { visit, currentURL } from '@ember/test-helpers';
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { currentURL, click } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { create } from 'ember-cli-page-object';
@@ -40,17 +45,8 @@ module('Acceptance | Enterprise | oidc auth namespace test', function (hooks) {
});
test('oidc: request is made to auth_url when a namespace is inputted', async function (assert) {
assert.expect(5);
assert.expect(4);
this.server.post(`/auth/${this.rootOidc}/oidc/auth_url`, (schema, req) => {
const { redirect_uri } = JSON.parse(req.requestBody);
const { pathname, search } = parseURL(redirect_uri);
assert.strictEqual(
pathname + search,
`/ui/vault/auth/${this.rootOidc}/oidc/callback`,
'request made to auth_url when the login page is visited'
);
});
this.server.post(`/auth/${this.nsOidc}/oidc/auth_url`, (schema, req) => {
const { redirect_uri } = JSON.parse(req.requestBody);
const { pathname, search } = parseURL(redirect_uri);
@@ -69,23 +65,24 @@ module('Acceptance | Enterprise | oidc auth namespace test', function (hooks) {
// enable oidc in child namespace with default role
await authPage.loginNs(this.namespace);
await this.enableOidc(this.nsOidc, `${this.nsOidc}-role`);
// check root namespace for method tab
await authPage.logout();
await visit('/vault/auth');
await authPage.namespaceInput('');
assert.dom(SELECTORS.authTab(this.rootOidc)).exists('renders oidc method tab for root');
// check child namespace for method tab
await authPage.namespaceInput(this.namespace);
assert.dom(SELECTORS.authTab(this.nsOidc)).exists('renders oidc method tab for child namespace');
// clicking on the tab should update with= queryParam
await click(`[data-test-auth-method="${this.nsOidc}"] a`);
assert.strictEqual(
currentURL(),
`/vault/auth?namespace=${this.namespace}&with=${this.nsOidc}%2F`,
'url updates with namespace value'
);
assert.dom(SELECTORS.authTab(this.nsOidc)).exists('renders oidc method tab for child namespace');
// disable methods to cleanup test state for re-running
await authPage.login();
await this.disableOidc(this.rootOidc);
await this.disableOidc(this.nsOidc);
await shell.runCommands([`delete /sys/auth/${this.namespace}`]);
await authPage.logout();
});
});

View File

@@ -17,7 +17,6 @@ import secretList from 'vault/tests/pages/secrets/backend/list';
import secretEdit from 'vault/tests/pages/secrets/backend/kv/edit-secret';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
module('Acceptance | leases', function (hooks) {
setupApplicationTest(hooks);
@@ -29,10 +28,6 @@ module('Acceptance | leases', function (hooks) {
return mountSecrets.visit().path(this.enginePath).type('kv').version(1).submit();
});
hooks.afterEach(function () {
return logout.visit();
});
const createSecret = async (context, isRenewable) => {
context.name = `secret-${uuidv4()}`;
await secretList.visitRoot({ backend: context.enginePath });

View File

@@ -7,7 +7,6 @@ import { module, test } from 'qunit';
import { currentURL, visit, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import Pretender from 'pretender';
import logout from 'vault/tests/pages/logout';
import { getManagedNamespace } from 'vault/routes/vault/cluster';
const FEATURE_FLAGS_RESPONSE = {
@@ -39,7 +38,6 @@ module('Acceptance | Enterprise | Managed namespace root', function (hooks) {
});
test('it shows the managed namespace toolbar when feature flag exists', async function (assert) {
await logout.visit();
await visit('/vault/auth');
assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth');
assert.ok(currentURL().includes('?namespace=admin'), 'with base namespace');
@@ -72,7 +70,6 @@ module('Acceptance | Enterprise | Managed namespace root', function (hooks) {
});
test('it redirects to root prefixed ns when non-root passed', async function (assert) {
await logout.visit();
await visit('/vault/auth?namespace=admindev');
assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth');
assert.ok(

View File

@@ -24,6 +24,32 @@ module('Acceptance | mfa-login-enforcement', function (hooks) {
ENV['ember-cli-mirage'].handler = null;
});
test('it should send the correct data when creating an enforcement', async function (assert) {
assert.expect(2);
this.server.post('/identity/mfa/login-enforcement/salad-college-setting', (schema, { requestBody }) => {
const data = JSON.parse(requestBody);
assert.deepEqual(data.auth_method_types, [], 'correctly passes empty array for auth method types');
assert.deepEqual(
data.auth_method_accessors,
['auth_userpass_bb95c2b1'],
'Passes correct value for auth method accessors'
);
return { ...data, id: data.name };
});
await visit('/ui/vault/access');
await click('[data-test-sidebar-nav-link="Multi-Factor Authentication"]');
await click('[data-test-tab="enforcements"]');
await click('[data-test-enforcement-create]');
// Fill out form
await fillIn('[data-test-mlef-input="name"]', 'salad-college-setting');
await click('[data-test-component="search-select"] .ember-basic-dropdown-trigger');
await click('.ember-power-select-option');
await fillIn('[data-test-mount-accessor-select]', 'auth_userpass_bb95c2b1');
await click('[data-test-mlef-add-target]');
await click('[data-test-mlef-save]');
});
test('it should create login enforcement', async function (assert) {
await visit('/ui/vault/access');
await click('[data-test-sidebar-nav-link="Multi-Factor Authentication"]');

View File

@@ -18,11 +18,17 @@ module('Acceptance | mfa-login', function (hooks) {
ENV['ember-cli-mirage'].handler = 'mfaLogin';
});
hooks.beforeEach(function () {
this.auth = this.owner.lookup('service:auth');
this.select = async (select = 0, option = 1) => {
const selector = `[data-test-mfa-select="${select}"]`;
const value = this.element.querySelector(`${selector} option:nth-child(${option + 1})`).value;
await fillIn(`${selector} select`, value);
};
return visit('/vault/logout');
});
hooks.afterEach(function () {
// Manually clear token after each so that future tests don't get into a weird state
this.auth.deleteCurrentToken();
});
hooks.after(function () {
ENV['ember-cli-mirage'].handler = null;

View File

@@ -7,7 +7,6 @@ import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, currentRouteName, currentURL, fillIn, visit } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import { setupMirage } from 'ember-cli-mirage/test-support';
import ENV from 'vault/config/environment';
import { Response } from 'miragejs';
@@ -27,7 +26,6 @@ module('Acceptance | mfa-method', function (hooks) {
methods.addObjects(this.server.db[`mfa${type}Methods`].where({}));
return methods;
}, []);
await logout.visit();
return authPage.login();
});
hooks.after(function () {

View File

@@ -7,7 +7,6 @@ import { visit } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import Ember from 'ember';
let adapterException;
@@ -23,7 +22,6 @@ module('Acceptance | not-found', function (hooks) {
hooks.afterEach(function () {
Ember.Test.adapter.exception = adapterException;
return logout.visit();
});
test('top-level not-found', async function (assert) {

View File

@@ -9,7 +9,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import ENV from 'vault/config/environment';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import { create } from 'ember-cli-page-object';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import ss from 'vault/tests/pages/components/search-select';
@@ -34,15 +33,11 @@ module('Acceptance | oidc-config clients and assignments', function (hooks) {
ENV['ember-cli-mirage'].handler = 'oidcConfig';
});
hooks.beforeEach(async function () {
this.store = await this.owner.lookup('service:store');
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
hooks.after(function () {
ENV['ember-cli-mirage'].handler = null;
});

View File

@@ -9,7 +9,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import ENV from 'vault/config/environment';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import { create } from 'ember-cli-page-object';
import { clickTrigger, selectChoose } from 'ember-power-select/test-support/helpers';
import ss from 'vault/tests/pages/components/search-select';
@@ -37,15 +36,11 @@ module('Acceptance | oidc-config clients and keys', function (hooks) {
ENV['ember-cli-mirage'].handler = 'oidcConfig';
});
hooks.beforeEach(async function () {
this.store = await this.owner.lookup('service:store');
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
hooks.after(function () {
ENV['ember-cli-mirage'].handler = null;
});

View File

@@ -9,7 +9,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import ENV from 'vault/config/environment';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import { create } from 'ember-cli-page-object';
import { clickTrigger, selectChoose } from 'ember-power-select/test-support/helpers';
import ss from 'vault/tests/pages/components/search-select';
@@ -39,17 +38,13 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
ENV['ember-cli-mirage'].handler = 'oidcConfig';
});
hooks.beforeEach(async function () {
this.store = await this.owner.lookup('service:store');
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
// mock client list so OIDC BASE URL does not redirect to landing call-to-action image
this.server.get('/identity/oidc/client', () => overrideMirageResponse(null, CLIENT_LIST_RESPONSE));
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
hooks.after(function () {
ENV['ember-cli-mirage'].handler = null;
});

View File

@@ -131,10 +131,9 @@ const setupOidc = async function (uid) {
module('Acceptance | oidc provider', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
hooks.beforeEach(function () {
this.uid = uuidv4();
this.store = await this.owner.lookup('service:store');
await logout.visit();
this.store = this.owner.lookup('service:store');
return authPage.login();
});

View File

@@ -33,7 +33,6 @@ module('Acceptance | pki action forms test', function (hooks) {
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
module('import', function (hooks) {

View File

@@ -34,7 +34,6 @@ module('Acceptance | pki configuration test', function (hooks) {
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
module('delete all issuers modal and empty states', function (hooks) {

View File

@@ -9,7 +9,6 @@ import { setupApplicationTest } from 'vault/tests/helpers';
import { v4 as uuidv4 } from 'uuid';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-issuer-cross-sign';
@@ -39,7 +38,6 @@ module('Acceptance | pki/pki cross sign', function (hooks) {
// Cleanup engine
await runCommands([`delete sys/mounts/${this.intMountPath}`]);
await runCommands([`delete sys/mounts/${this.parentMountPath}`]);
await logout.visit();
});
test('it cross-signs an issuer', async function (assert) {

View File

@@ -35,7 +35,6 @@ module('Acceptance | pki engine route cleanup test', function (hooks) {
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
module('configuration', function () {

View File

@@ -38,7 +38,6 @@ module('Acceptance | pki workflow', function (hooks) {
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
test('empty state messages are correct when PKI not configured', async function (assert) {

View File

@@ -49,7 +49,6 @@ module('Acceptance | pki overview', function (hooks) {
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
test('navigates to view issuers when link is clicked on issuer card', async function (assert) {

View File

@@ -37,7 +37,6 @@ module('Acceptance | pki tidy', function (hooks) {
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
test('it configures a manual tidy operation and shows its details and tidy states', async function (assert) {

View File

@@ -7,7 +7,6 @@ import { currentURL, currentRouteName, visit } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
module('Acceptance | policies', function (hooks) {
setupApplicationTest(hooks);
@@ -16,10 +15,6 @@ module('Acceptance | policies', function (hooks) {
return authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
test('it redirects to acls with unknown policy type', async function (assert) {
await visit('/vault/policies/foo');
assert.strictEqual(currentRouteName(), 'vault.cluster.policies.index');

View File

@@ -8,7 +8,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, visit } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
module('Acceptance | raft storage', function (hooks) {
setupApplicationTest(hooks);
@@ -22,9 +21,6 @@ module('Acceptance | raft storage', function (hooks) {
this.server.get('/sys/license/features', () => ({}));
await authPage.login();
});
hooks.afterEach(function () {
return logout.visit();
});
test('it should render correct number of raft peers', async function (assert) {
assert.expect(3);

View File

@@ -12,28 +12,21 @@ import { selectChoose, clickTrigger } from 'ember-power-select/test-support/help
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import connectionPage from 'vault/tests/pages/secrets/backend/database/connection';
import rolePage from 'vault/tests/pages/secrets/backend/database/role';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import searchSelect from 'vault/tests/pages/components/search-select';
import {
createPolicyCmd,
deleteEngineCmd,
mountEngineCmd,
runCmd,
tokenWithPolicyCmd,
} from 'vault/tests/helpers/commands';
const searchSelectComponent = create(searchSelect);
const consoleComponent = create(consoleClass);
const MODEL = {
engineType: 'database',
id: 'database-name',
};
const mount = async () => {
const path = `database-${Date.now()}`;
await mountSecrets.enable('database', path);
await settled();
return path;
};
const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
const name = `connection-${Date.now()}`;
await connectionPage.visitCreate({ backend });
@@ -46,6 +39,14 @@ const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
return name;
};
const navToConnection = async (backend, connection) => {
await visit('/vault/secrets');
await click(`[data-test-auth-backend-link="${backend}"]`);
await click('[data-test-secret-list-tab="Connections"]');
await click(`[data-test-secret-link="${connection}"]`);
return;
};
const connectionTests = [
{
name: 'elasticsearch-connection',
@@ -53,6 +54,7 @@ const connectionTests = [
elasticUser: 'username',
elasticPassword: 'password',
url: 'http://127.0.0.1:9200',
assertCount: 9,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -71,6 +73,7 @@ const connectionTests = [
name: 'mongodb-connection',
plugin: 'mongodb-database-plugin',
url: `mongodb://127.0.0.1:4321/test`,
assertCount: 5,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -85,6 +88,7 @@ const connectionTests = [
name: 'mssql-connection',
plugin: 'mssql-database-plugin',
url: `mssql://127.0.0.1:4321/test`,
assertCount: 6,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -106,6 +110,7 @@ const connectionTests = [
name: 'mysql-connection',
plugin: 'mysql-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
assertCount: 7,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -128,6 +133,7 @@ const connectionTests = [
name: 'mysql-aurora-connection',
plugin: 'mysql-aurora-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
assertCount: 7,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -150,6 +156,7 @@ const connectionTests = [
name: 'mysql-rds-connection',
plugin: 'mysql-rds-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
assertCount: 7,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -172,6 +179,7 @@ const connectionTests = [
name: 'mysql-legacy-connection',
plugin: 'mysql-legacy-database-plugin',
url: `{{username}}:{{password}}@tcp(127.0.0.1:3306)/test`,
assertCount: 7,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -194,6 +202,7 @@ const connectionTests = [
name: 'postgresql-connection',
plugin: 'postgresql-database-plugin',
url: `postgresql://{{username}}:{{password}}@localhost:5432/postgres?sslmode=disable`,
assertCount: 7,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
@@ -214,45 +223,18 @@ const connectionTests = [
.exists(`Username template toggle exists for ${name}`);
},
},
// keep oracle as last DB because it is skipped in some tests (line 285) the UI doesn't return to empty state after
{
name: 'oracle-connection',
plugin: 'vault-plugin-database-oracle',
url: `{{username}}/{{password}}@localhost:1521/OraDoc.localhost`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
assert
.dom('[data-test-database-oracle-alert]')
.hasTextContaining(
`Warning Please ensure that your Oracle plugin has the default name of vault-plugin-database-oracle. Custom naming is not supported in the UI at this time. If the plugin is already named vault-plugin-database-oracle, disregard this warning.`,
'warning banner displays for oracle plugin name'
);
},
},
];
module('Acceptance | secrets/database/*', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
this.server = apiStub({ usePassthrough: true });
return authPage.login();
this.backend = `database-testing`;
await authPage.login();
return consoleComponent.runCommands(mountEngineCmd('database', this.backend));
});
hooks.afterEach(function () {
this.server.shutdown();
return consoleComponent.runCommands(deleteEngineCmd(this.backend));
});
test('can enable the database secrets engine', async function (assert) {
@@ -272,12 +254,15 @@ module('Acceptance | secrets/database/*', function (hooks) {
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/overview`, 'Tab links to overview page');
assert.dom('[data-test-component="empty-state"]').exists('Empty state also exists on overview page');
assert.dom('[data-test-secret-list-tab="Roles"]').exists('Has Roles tab');
await visit('/vault/secrets');
// Cleanup backend
await consoleComponent.runCommands(deleteEngineCmd(backend));
});
test('Connection create and edit form for each plugin', async function (assert) {
assert.expect(161);
const backend = await mount();
for (const testCase of connectionTests) {
for (const testCase of connectionTests) {
test(`database connection create and edit: ${testCase.plugin}`, async function (assert) {
assert.expect(19 + testCase.assertCount);
const backend = this.backend;
await connectionPage.visitCreate({ backend });
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Correct creation URL');
assert
@@ -293,19 +278,20 @@ module('Acceptance | secrets/database/*', function (hooks) {
} else {
await connectionPage.connectionUrl(testCase.url);
}
// skip adding oracle db connection since plugin doesn't exist
if (testCase.plugin === 'vault-plugin-database-oracle') {
testCase.requiredFields(assert, testCase.name);
continue;
}
testCase.requiredFields(assert, testCase.name);
testCase.requiredFields(assert, testCase.plugin);
assert.dom('[data-test-input="verify_connection"]').isChecked('verify is checked');
await connectionPage.toggleVerify();
assert.dom('[data-test-input="verify_connection"]').isNotChecked('verify is unchecked');
assert
.dom('[data-test-database-oracle-alert]')
.doesNotExist('does not show oracle alert for non-oracle plugins');
await connectionPage.save();
await settled();
assert
.dom('.modal.is-active .title')
.hasText('Rotate your root credentials?', 'Modal appears asking to rotate root credentials');
await connectionPage.enable();
assert.dom('[data-test-enable-connection]').exists('Enable button exists');
await click('[data-test-enable-connection]');
assert.ok(
currentURL().startsWith(`/vault/secrets/${backend}/show/${testCase.name}`),
`Saves connection and takes you to show page for ${testCase.name}`
@@ -322,8 +308,10 @@ module('Acceptance | secrets/database/*', function (hooks) {
assert.dom(`[data-test-input="plugin_name"]`).hasAttribute('readonly');
assert.dom('[data-test-input="password"]').doesNotExist('Password is not displayed on edit form');
assert.dom('[data-test-toggle-input="show-password"]').exists('Update password toggle exists');
await connectionPage.toggleVerify();
assert.dom('[data-test-input="verify_connection"]').isNotChecked('verify is still unchecked');
await connectionPage.save();
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/show/${testCase.name}`);
// click "Add Role"
await connectionPage.addRole();
await settled();
@@ -332,12 +320,58 @@ module('Acceptance | secrets/database/*', function (hooks) {
testCase.name,
'Database connection is pre-selected on the form'
);
await click('[data-test-secret-breadcrumb]');
}
await click('[data-test-database-role-cancel]');
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/list`, 'Cancel button links to list view');
});
}
test('database connection create and edit: vault-plugin-database-oracle', async function (assert) {
assert.expect(11);
// keep oracle as separate test because it behaves differently than the others
const testCase = {
name: 'oracle-connection',
plugin: 'vault-plugin-database-oracle',
url: `{{username}}/{{password}}@localhost:1521/OraDoc.localhost`,
requiredFields: async (assert, name) => {
assert.dom('[data-test-input="username"]').exists(`Username field exists for ${name}`);
assert.dom('[data-test-input="password"]').exists(`Password field exists for ${name}`);
assert
.dom('[data-test-input="max_open_connections"]')
.exists(`Max open connections exists for ${name}`);
assert
.dom('[data-test-input="max_idle_connections"]')
.exists(`Max idle connections exists for ${name}`);
assert
.dom('[data-test-input="max_connection_lifetime"]')
.exists(`Max connection lifetime exists for ${name}`);
assert
.dom('[data-test-input="root_rotation_statements"]')
.exists(`Root rotation statements exists for ${name}`);
assert
.dom('[data-test-database-oracle-alert]')
.hasTextContaining(
`Warning Please ensure that your Oracle plugin has the default name of vault-plugin-database-oracle. Custom naming is not supported in the UI at this time. If the plugin is already named vault-plugin-database-oracle, disregard this warning.`,
'warning banner displays for oracle plugin name'
);
},
};
const backend = this.backend;
await connectionPage.visitCreate({ backend });
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Correct creation URL');
assert
.dom('[data-test-empty-state-title]')
.hasText('No plugin selected', 'No plugin is selected by default and empty state shows');
await connectionPage.dbPlugin(testCase.plugin);
assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected');
assert.dom('[data-test-database-oracle-alert]').exists('shows oracle alert');
await connectionPage.name(testCase.name);
await connectionPage.connectionUrl(testCase.url);
testCase.requiredFields(assert, testCase.plugin);
// Cannot save without plugin mounted
// TODO: add fake server response for fuller test coverage
});
test('Can create and delete a connection', async function (assert) {
const backend = await mount();
const backend = this.backend;
const connectionDetails = {
plugin: 'mongodb-database-plugin',
id: 'horses-db',
@@ -349,11 +383,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
{ label: 'Write concern', name: 'write_concern' },
],
};
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/list`,
'Mounts and redirects to connection list page'
);
await visit(`/vault/secrets/${backend}/list`);
await connectionPage.createLink();
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`, 'Create link goes to create page');
assert
@@ -407,9 +437,25 @@ module('Acceptance | secrets/database/*', function (hooks) {
});
test('buttons show up for managing connection', async function (assert) {
const backend = await mount();
const backend = this.backend;
const connection = await newConnection(backend);
await connectionPage.visitShow({ backend, id: connection });
const CONNECTION_VIEW_ONLY = `
path "${backend}/config" {
capabilities = ["list"]
}
path "${backend}/config/*" {
capabilities = ["read"]
}
# allow backend cleanup on afterEach
path "sys/mounts/${backend}" {
capabilities = ["delete"]
}
`;
const token = await runCmd(consoleComponent, [
...createPolicyCmd('test-policy', CONNECTION_VIEW_ONLY),
...tokenWithPolicyCmd('test-policy'),
]);
await navToConnection(backend, connection);
assert
.dom('[data-test-database-connection-delete]')
.hasText('Delete connection', 'Delete connection button exists with correct text');
@@ -418,26 +464,11 @@ module('Acceptance | secrets/database/*', function (hooks) {
.hasText('Reset connection', 'Reset button exists with correct text');
assert.dom('[data-test-secret-create]').hasText('Add role', 'Add role button exists with correct text');
assert.dom('[data-test-edit-link]').hasText('Edit configuration', 'Edit button exists with correct text');
const CONNECTION_VIEW_ONLY = `
path "${backend}/*" {
capabilities = ["deny"]
}
path "${backend}/config" {
capabilities = ["list"]
}
path "${backend}/config/*" {
capabilities = ["read"]
}
`;
await consoleComponent.runCommands([
`write sys/mounts/${backend} type=database`,
`write sys/policies/acl/test-policy policy=${btoa(CONNECTION_VIEW_ONLY)}`,
'write -field=client_token auth/token/create policies=test-policy ttl=1h',
]);
const token = consoleComponent.lastTextOutput;
await logout.visit();
await authPage.logout();
// Check with restricted permissions
await authPage.login(token);
await connectionPage.visitShow({ backend, id: connection });
assert.dom(`[data-test-auth-backend-link="${backend}"]`).exists('Shows backend on secret list page');
await navToConnection(backend, connection);
assert.strictEqual(
currentURL(),
`/vault/secrets/${backend}/show/${connection}`,
@@ -463,7 +494,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
});
test('Role create form', async function (assert) {
const backend = await mount();
const backend = this.backend;
// Connection needed for role fields
await newConnection(backend);
await rolePage.visitCreate({ backend });
@@ -494,49 +525,40 @@ module('Acceptance | secrets/database/*', function (hooks) {
});
test('root and limited access', async function (assert) {
this.set('model', MODEL);
const backend = 'database';
const backend = this.backend;
const NO_ROLES_POLICY = `
path "database/roles/*" {
path "${backend}/roles/*" {
capabilities = ["delete"]
}
path "database/static-roles/*" {
path "${backend}/static-roles/*" {
capabilities = ["delete"]
}
path "database/config/*" {
path "${backend}/config/*" {
capabilities = ["list", "create", "read", "update"]
}
path "database/creds/*" {
path "${backend}/creds/*" {
capabilities = ["list", "create", "read", "update"]
}
`;
await consoleComponent.runCommands([
`write sys/mounts/${backend} type=database`,
`write sys/policies/acl/test-policy policy=${btoa(NO_ROLES_POLICY)}`,
'write -field=client_token auth/token/create policies=test-policy ttl=1h',
const token = await runCmd(consoleComponent, [
...createPolicyCmd('test-policy', NO_ROLES_POLICY),
...tokenWithPolicyCmd('test-policy'),
]);
const token = consoleComponent.lastTextOutput;
// test root user flow
await settled();
// await click('[data-test-secret-backend-row="database"]');
// skipping the click because occasionally is shows up on the second page and cannot be found
await visit(`/vault/secrets/database/overview`);
// test root user flow first
await visit(`/vault/secrets/${backend}/overview`);
assert.dom('[data-test-component="empty-state"]').exists('renders empty state');
assert.dom('[data-test-secret-list-tab="Connections"]').exists('renders connections tab');
assert.dom('[data-test-secret-list-tab="Roles"]').exists('renders connections tab');
await click('[data-test-secret-create="connections"]');
assert.strictEqual(currentURL(), '/vault/secrets/database/create');
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create`);
// Login with restricted policy
await logout.visit();
await authPage.login(token);
await settled();
// skipping the click because occasionally is shows up on the second page and cannot be found
await visit(`/vault/secrets/database/overview`);
await visit(`/vault/secrets/${backend}/overview`);
assert.dom('[data-test-tab="overview"]').exists('renders overview tab');
assert.dom('[data-test-secret-list-tab="Connections"]').exists('renders connections tab');
assert
@@ -547,6 +569,6 @@ module('Acceptance | secrets/database/*', function (hooks) {
.exists({ count: 1 }, 'renders only the connection card');
await click('[data-test-action-text="Configure new"]');
assert.strictEqual(currentURL(), '/vault/secrets/database/create?itemType=connection');
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create?itemType=connection`);
});
});

View File

@@ -74,7 +74,6 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
hooks.afterEach(async function () {
this.server.shutdown();
await logout.visit();
});
test('it creates a secret and redirects', async function (assert) {
@@ -682,7 +681,8 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
await deleteEngine(enginePath, assert);
});
test('version 2: with metadata no read or list but with delete access and full access to the data endpoint', async function (assert) {
// TODO VAULT-16258: revisit when KV-V2 is engine
test.skip('version 2: with metadata no read or list but with delete access and full access to the data endpoint', async function (assert) {
assert.expect(12);
const enginePath = 'no-metadata-read';
const secretPath = 'no-metadata-read-secret-name';

Some files were not shown because too many files have changed in this diff Show More