mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
* adds error handling for control groups to api service as post request middleware * updates kv list route to use api service * updates kv config route to use api service * updates kv secrets overview route to use api service * updates kv secret details route to use api service * adds kv form * updates kv metadata details route to use api service * updates kv paths and version history routes to use api service * refactors kv-data-fields component to form component * updates kv secret create route to use api service * updates kv secret edit route to use api service * updates kv metadata edit route to use api service * adds waitFor to async middleware in api service to attempt to fix race conditions in tests * adds kvMetadata path to capabilities path map * fixes kv list item delete test selector * removes kv models, adapters and serializers * removes store from kv addon * removes ember data related test helpers from kv-run-commands * updates comments that referred to kv ember data models * updates kv-page-header tests * updates model-form-fields test to use totp-key model rather than kv/data * removes another reference to kv/data model from path-help test * fixes kv v2 workflow create tests * fixes issue returning metadata for secret when latest version is deleted * decodes uri in path returned by api service parseError method * fixes kv v2 edge cases tests * fixes issue deleteing control group token in api service * decodes url for control group token lookup in api service * fixes version history linked block link * defaults cas to 0 when creating new secret * removes log * adds ember-template-lint to kv engine * more test fixes * updates kv-suggestion-input component to use api service * removes kv metadata model reference from quick actions card * fixes sync destination sync secrets tests * updates kv helpers from classic format * updates kv helpers imports * reverts to use secret.version in details edit route * fixes isDeleted import in kv version history test * adds waitFor to api service parseError method * reverts removing async from addQueryParams api method * attempts to fix test flakiness requesting custom metadata from data endpoint * more tweaks to requesting metadata from data * adds waitFor to requestData method Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
parent
0c3dcbc30e
commit
bf32d52450
34 changed files with 269 additions and 1742 deletions
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationAdapter from '../application';
|
||||
export default class KvConfigAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
urlForFindRecord(id) {
|
||||
return `${this.buildURL()}/${id}/config`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationAdapter from '../application';
|
||||
import { kvDataPath, kvDeletePath, kvDestroyPath, kvSubkeysPath, kvUndeletePath } from 'vault/utils/kv-path';
|
||||
import { assert } from '@ember/debug';
|
||||
import ControlGroupError from 'vault/lib/control-group-error';
|
||||
|
||||
export default class KvDataAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
_url(fullPath) {
|
||||
return `${this.buildURL()}/${fullPath}`;
|
||||
}
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const { backend, path } = snapshot.record;
|
||||
const url = this._url(kvDataPath(backend, path));
|
||||
|
||||
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((res) => {
|
||||
return {
|
||||
data: {
|
||||
id: kvDataPath(backend, path, res.data.version),
|
||||
backend,
|
||||
path,
|
||||
...res.data,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fetchSubkeys(backend, path, query) {
|
||||
const url = this._url(kvSubkeysPath(backend, path, query));
|
||||
return (
|
||||
this.ajax(url, 'GET')
|
||||
.then((resp) => resp.data)
|
||||
// deleted/destroyed secret versions throw an error
|
||||
// but still have metadata that we want to return
|
||||
.catch((errorOrResponse) => {
|
||||
return this.parseErrorOrResponse(errorOrResponse, { backend, path }, true);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fetchWrapInfo(query) {
|
||||
const { backend, path, version, wrapTTL } = query;
|
||||
const id = kvDataPath(backend, path, version);
|
||||
return this.ajax(this._url(id), 'GET', { wrapTTL }).then((resp) => resp.wrap_info);
|
||||
}
|
||||
|
||||
// patching a secret happens without retrieving the ember data model
|
||||
// so we use a custom method instead of updateRecord
|
||||
patchSecret(backend, path, patchData, version) {
|
||||
const url = this._url(kvDataPath(backend, path));
|
||||
const data = {
|
||||
options: { cas: version },
|
||||
data: patchData,
|
||||
};
|
||||
return this.ajax(url, 'PATCH', { data });
|
||||
}
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, path, version } = query;
|
||||
// ID is the full path for the data (including version)
|
||||
let id = kvDataPath(backend, path, version);
|
||||
return this.ajax(this._url(id), 'GET')
|
||||
.then((resp) => {
|
||||
// if no version is queried, add version from response to ID
|
||||
// otherwise duplicate ember data models will exist in store
|
||||
// (one with an ID that includes the version and one without)
|
||||
if (!version) {
|
||||
id = kvDataPath(backend, path, resp.data.metadata.version);
|
||||
}
|
||||
return {
|
||||
...resp,
|
||||
data: {
|
||||
id,
|
||||
backend,
|
||||
path,
|
||||
...resp.data,
|
||||
},
|
||||
};
|
||||
})
|
||||
.catch((errorOrResponse) => {
|
||||
return this.parseErrorOrResponse(errorOrResponse, { id, backend, path, version });
|
||||
});
|
||||
}
|
||||
|
||||
/* Five types of delete operations */
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { backend, path } = snapshot.record;
|
||||
const { deleteType, deleteVersions } = snapshot.adapterOptions;
|
||||
|
||||
if (!backend || !path) {
|
||||
throw new Error('The request to delete or undelete is missing required attributes.');
|
||||
}
|
||||
|
||||
switch (deleteType) {
|
||||
case 'delete-latest-version':
|
||||
return this.ajax(this._url(kvDataPath(backend, path)), 'DELETE');
|
||||
case 'delete-version':
|
||||
return this.ajax(this._url(kvDeletePath(backend, path)), 'POST', {
|
||||
data: { versions: deleteVersions },
|
||||
});
|
||||
case 'destroy':
|
||||
return this.ajax(this._url(kvDestroyPath(backend, path)), 'PUT', {
|
||||
data: { versions: deleteVersions },
|
||||
});
|
||||
case 'undelete':
|
||||
return this.ajax(this._url(kvUndeletePath(backend, path)), 'POST', {
|
||||
data: { versions: deleteVersions },
|
||||
});
|
||||
default:
|
||||
assert('deleteType must be one of delete-latest-version, delete-version, destroy, or undelete.');
|
||||
}
|
||||
}
|
||||
|
||||
handleResponse(status, headers, payload, requestData) {
|
||||
// after deleting a secret version, data is null and the API returns a 404
|
||||
// but there could be relevant metadata
|
||||
if (status === 404 && payload.data?.metadata) {
|
||||
return super.handleResponse(200, headers, payload, requestData);
|
||||
}
|
||||
return super.handleResponse(...arguments);
|
||||
}
|
||||
|
||||
parseErrorOrResponse(errorOrResponse, secretDataBaseResponse, isSubkeys = false) {
|
||||
// if it's a legitimate error - throw it!
|
||||
if (errorOrResponse instanceof ControlGroupError) {
|
||||
throw errorOrResponse;
|
||||
}
|
||||
|
||||
const errorCode = errorOrResponse.httpStatus;
|
||||
if (errorCode === 403) {
|
||||
return {
|
||||
data: {
|
||||
...secretDataBaseResponse,
|
||||
fail_read_error_code: errorCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// in the case of a deleted/destroyed secret the API returns a 404 because { data: null }
|
||||
// however, there could be a metadata block with important information like deletion_time
|
||||
// handleResponse below checks 404 status codes for metadata and updates the code to 200 if it exists.
|
||||
// we still end up in the good ol' catch() block, but instead of a 404 adapter error we've "caught"
|
||||
// the metadata that sneakily tried to hide from us
|
||||
if (errorOrResponse.data) {
|
||||
// subkeys response doesn't correspond to a model, no need to include base response
|
||||
if (isSubkeys) return errorOrResponse.data;
|
||||
|
||||
return {
|
||||
...errorOrResponse,
|
||||
data: {
|
||||
...secretDataBaseResponse,
|
||||
...errorOrResponse.data, // includes the { metadata } key we want
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// If we get here, it's probably a 404 because it doesn't exist
|
||||
throw errorOrResponse;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationAdapter from '../application';
|
||||
import { kvMetadataPath } from 'vault/utils/kv-path';
|
||||
|
||||
export default class KvMetadataAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
_url(fullPath) {
|
||||
return `${this.buildURL()}/${fullPath}`;
|
||||
}
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const { backend, path } = snapshot.record;
|
||||
const id = kvMetadataPath(backend, path);
|
||||
const url = this._url(id);
|
||||
const data = this.serialize(snapshot);
|
||||
return this.ajax(url, 'POST', { data }).then(() => {
|
||||
return {
|
||||
id,
|
||||
data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
updateRecord(store, type, snapshot) {
|
||||
const { backend, path } = snapshot.record;
|
||||
const id = kvMetadataPath(backend, path);
|
||||
const url = this._url(id);
|
||||
const data = this.serialize(snapshot);
|
||||
return this.ajax(url, 'POST', { data }).then(() => {
|
||||
return {
|
||||
id,
|
||||
data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
query(store, type, query) {
|
||||
const { backend, pathToSecret } = query;
|
||||
// example of pathToSecret: beep/boop/
|
||||
return this.ajax(this._url(kvMetadataPath(backend, pathToSecret)), 'GET', {
|
||||
data: { list: true },
|
||||
}).then((resp) => {
|
||||
resp.backend = backend;
|
||||
resp.path = pathToSecret;
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, path } = query;
|
||||
// ID is the full path for the metadata
|
||||
const id = kvMetadataPath(backend, path);
|
||||
return this.ajax(this._url(id), 'GET').then((resp) => {
|
||||
return {
|
||||
id,
|
||||
...resp,
|
||||
data: {
|
||||
backend,
|
||||
path,
|
||||
...resp.data,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// This method is called when deleting from the list or metadata details view.
|
||||
// Otherwise, delete happens in kv/data adapter
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { backend, path, fullSecretPath } = snapshot.record;
|
||||
// fullSecretPath is used when deleting from the LIST view and is defined via the serializer
|
||||
// path is used when deleting from the metadata details view.
|
||||
return this.ajax(this._url(kvMetadataPath(backend, fullSecretPath || path)), 'DELETE');
|
||||
}
|
||||
|
||||
// custom method used if users do not have "read" permissions to fetch record
|
||||
deleteMetadata(backend, path) {
|
||||
return this.ajax(this._url(kvMetadataPath(backend, path)), 'DELETE');
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +105,6 @@ export default class App extends Application {
|
|||
'namespace',
|
||||
{ 'app-router': 'router' },
|
||||
'secret-mount-path',
|
||||
'store',
|
||||
'pagination',
|
||||
'version',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -39,16 +39,16 @@
|
|||
@ariaLabel="Action"
|
||||
/>
|
||||
|
||||
{{#if this.searchSelectParams.model}}
|
||||
{{#if this.selectedAction}}
|
||||
{{! use special input to allow searching for KVv2 secrets inside a directory }}
|
||||
{{#if (eq this.selectedEngine.type "kv")}}
|
||||
{{#if this.searchSelectParams.isKV}}
|
||||
<KvSuggestionInput
|
||||
@label="Secret Path"
|
||||
@value={{this.paramValue}}
|
||||
@mountPath={{this.selectedEngine.id}}
|
||||
@onChange={{fn (mut this.paramValue)}}
|
||||
/>
|
||||
{{else}}
|
||||
{{else if this.searchSelectParams.model}}
|
||||
<h3 class="title is-6" data-test-card-subtitle="param">{{this.searchSelectParams.title}}</h3>
|
||||
|
||||
<SearchSelect
|
||||
|
|
|
|||
|
|
@ -45,14 +45,9 @@ export default class DashboardQuickActionsCard extends Component {
|
|||
switch (this.selectedAction) {
|
||||
case 'Find KV secrets':
|
||||
return {
|
||||
title: 'Secret path',
|
||||
subText: 'Path of the secret you want to read.',
|
||||
isKV: true,
|
||||
buttonText: 'Read secrets',
|
||||
model: 'kv/metadata',
|
||||
route: 'vault.cluster.secrets.backend.kv.secret.index',
|
||||
nameKey: 'path',
|
||||
queryObject: { pathToSecret: '', backend: this.selectedEngine.id },
|
||||
objectKeys: ['path', 'id'],
|
||||
};
|
||||
case 'Generate credentials for database':
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
import { duration } from 'core/helpers/format-duration';
|
||||
|
||||
// This model is used only for display only - configuration happens via secret-engine model when an engine is mounted
|
||||
@withFormFields(['casRequired', 'deleteVersionAfter', 'maxVersions'])
|
||||
export default class KvConfigModel extends Model {
|
||||
@attr backend;
|
||||
@attr('number', { label: 'Maximum number of versions' }) maxVersions;
|
||||
|
||||
@attr('boolean', { label: 'Require check and set' }) casRequired;
|
||||
|
||||
@attr({ label: 'Automate secret deletion' }) deleteVersionAfter;
|
||||
|
||||
@lazyCapabilities(apiPath`${'backend'}/config`, 'backend') configPath;
|
||||
|
||||
get canRead() {
|
||||
return this.configPath.get('canRead') !== false;
|
||||
}
|
||||
|
||||
// used in template to render using this model instead of secret-engine (where these attrs also exist)
|
||||
get displayFields() {
|
||||
return ['casRequired', 'deleteVersionAfter', 'maxVersions'];
|
||||
}
|
||||
|
||||
get displayDeleteTtl() {
|
||||
if (this.deleteVersionAfter === '0s') return 'Never delete';
|
||||
return duration([this.deleteVersionAfter]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
import { isDeleted } from 'kv/helpers/is-deleted';
|
||||
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
|
||||
|
||||
/* sample response
|
||||
{
|
||||
"data": {
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"metadata": {
|
||||
"created_time": "2018-03-22T02:24:06.945319214Z",
|
||||
"custom_metadata": {
|
||||
"owner": "jdoe",
|
||||
"mission_critical": "false"
|
||||
},
|
||||
"deletion_time": "",
|
||||
"destroyed": false,
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const validations = {
|
||||
path: [
|
||||
{ type: 'presence', message: `Path can't be blank.` },
|
||||
{ type: 'endsInSlash', message: `Path can't end in forward slash '/'.` },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: WHITESPACE_WARNING('path'),
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
secretData: [
|
||||
{
|
||||
validator: (model) =>
|
||||
model.secretData !== undefined && typeof model.secretData !== 'object' ? false : true,
|
||||
message: 'Vault expects data to be formatted as an JSON object.',
|
||||
},
|
||||
],
|
||||
};
|
||||
@withModelValidations(validations)
|
||||
@withFormFields()
|
||||
export default class KvSecretDataModel extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord.
|
||||
@attr('string', {
|
||||
label: 'Path for this secret',
|
||||
subText: 'Names with forward slashes define hierarchical path structures.',
|
||||
})
|
||||
path;
|
||||
@attr('object') secretData; // { key: value } data of the secret version
|
||||
|
||||
// Params returned on the GET response.
|
||||
@attr('string') createdTime;
|
||||
@attr('object') customMetadata;
|
||||
@attr('string') deletionTime;
|
||||
@attr('boolean') destroyed;
|
||||
@attr('number') version;
|
||||
// Set in adapter if read failed
|
||||
@attr('number') failReadErrorCode;
|
||||
|
||||
// if creating a new version this value is set in the edit route's
|
||||
// model hook from metadata or secret version, pending permissions
|
||||
// if the value is not a number, don't send options.cas on payload
|
||||
@attr('number')
|
||||
casVersion;
|
||||
|
||||
get state() {
|
||||
if (this.destroyed) return 'destroyed';
|
||||
if (this.isSecretDeleted) return 'deleted';
|
||||
if (this.createdTime) return 'created';
|
||||
return '';
|
||||
}
|
||||
|
||||
// cannot use isDeleted as model property name because of an ember property conflict
|
||||
get isSecretDeleted() {
|
||||
return isDeleted(this.deletionTime);
|
||||
}
|
||||
|
||||
// Permissions
|
||||
@lazyCapabilities(apiPath`${'backend'}/data/${'path'}`, 'backend', 'path') dataPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/metadata/${'path'}`, 'backend', 'path') metadataPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/delete/${'path'}`, 'backend', 'path') deletePath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/destroy/${'path'}`, 'backend', 'path') destroyPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/undelete/${'path'}`, 'backend', 'path') undeletePath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/subkeys/${'path'}`, 'backend', 'path') subkeysPath;
|
||||
|
||||
get canDeleteLatestVersion() {
|
||||
return this.dataPath.get('canDelete') !== false;
|
||||
}
|
||||
get canDeleteVersion() {
|
||||
return this.deletePath.get('canUpdate') !== false;
|
||||
}
|
||||
get canUndelete() {
|
||||
return this.undeletePath.get('canUpdate') !== false;
|
||||
}
|
||||
get canDestroyVersion() {
|
||||
return this.destroyPath.get('canUpdate') !== false;
|
||||
}
|
||||
get canEditData() {
|
||||
return this.dataPath.get('canUpdate') !== false;
|
||||
}
|
||||
get canReadData() {
|
||||
return this.dataPath.get('canRead') !== false;
|
||||
}
|
||||
get canReadMetadata() {
|
||||
return this.metadataPath.get('canRead') !== false;
|
||||
}
|
||||
get canUpdateMetadata() {
|
||||
return this.metadataPath.get('canUpdate') !== false;
|
||||
}
|
||||
get canListMetadata() {
|
||||
return this.metadataPath.get('canList') !== false;
|
||||
}
|
||||
get canDeleteMetadata() {
|
||||
return this.metadataPath.get('canDelete') !== false;
|
||||
}
|
||||
get canReadSubkeys() {
|
||||
return this.subkeysPath.get('canRead') !== false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
import { keyIsFolder } from 'core/utils/key-utils';
|
||||
import { isDeleted } from 'kv/helpers/is-deleted';
|
||||
|
||||
const validations = {
|
||||
maxVersions: [
|
||||
{ type: 'number', message: 'Maximum versions must be a number.' },
|
||||
{ type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' },
|
||||
],
|
||||
};
|
||||
const formFieldProps = ['customMetadata', 'maxVersions', 'casRequired', 'deleteVersionAfter'];
|
||||
|
||||
@withModelValidations(validations)
|
||||
@withFormFields(formFieldProps)
|
||||
export default class KvSecretMetadataModel extends Model {
|
||||
@attr('string') backend;
|
||||
@attr('string') path;
|
||||
@attr('string') fullSecretPath;
|
||||
|
||||
@attr('number', {
|
||||
defaultValue: 0,
|
||||
label: 'Maximum number of versions',
|
||||
subText:
|
||||
'The number of versions to keep per key. Once the number of keys exceeds the maximum number set here, the oldest version will be permanently deleted.',
|
||||
})
|
||||
maxVersions;
|
||||
|
||||
@attr('boolean', {
|
||||
defaultValue: false,
|
||||
label: 'Require Check and Set',
|
||||
subText: `Writes will only be allowed if the key's current version matches the version specified in the cas parameter.`,
|
||||
})
|
||||
casRequired;
|
||||
|
||||
@attr('string', {
|
||||
defaultValue: '0s',
|
||||
editType: 'ttl',
|
||||
label: 'Automate secret deletion',
|
||||
helperTextDisabled: `A secret's version must be manually deleted.`,
|
||||
helperTextEnabled: 'Delete all new versions of this secret after:',
|
||||
})
|
||||
deleteVersionAfter;
|
||||
|
||||
// the API returns custom_metadata: null if empty but because the attr is an 'object' ember data transforms it to an empty object.
|
||||
// this is important because we rely on the empty object as a truthy value in template conditionals
|
||||
@attr('object', {
|
||||
editType: 'kv',
|
||||
isSectionHeader: true,
|
||||
subText: 'An optional set of informational key-value pairs that will be stored with all secret versions.',
|
||||
})
|
||||
customMetadata;
|
||||
|
||||
// Additional Params only returned on the GET response.
|
||||
@attr('string') createdTime;
|
||||
@attr('number') currentVersion;
|
||||
@attr('number') oldestVersion;
|
||||
@attr('string') updatedTime;
|
||||
@attr('object') versions;
|
||||
|
||||
// used for KV list and list-directory view
|
||||
get pathIsDirectory() {
|
||||
// ex: beep/
|
||||
return keyIsFolder(this.path);
|
||||
}
|
||||
|
||||
// turns version object into an array for version dropdown menu
|
||||
get sortedVersions() {
|
||||
const array = [];
|
||||
for (const key in this.versions) {
|
||||
this.versions[key].isSecretDeleted = isDeleted(this.versions[key].deletion_time);
|
||||
array.push({ version: key, ...this.versions[key] });
|
||||
}
|
||||
// version keys are in order created with 1 being the oldest, we want newest first
|
||||
return array.reverse();
|
||||
}
|
||||
|
||||
// helps in long logic statements for state of a currentVersion
|
||||
get currentSecret() {
|
||||
if (!this.versions || !this.currentVersion) return false;
|
||||
const data = this.versions[this.currentVersion];
|
||||
const state = data.destroyed ? 'destroyed' : isDeleted(data.deletion_time) ? 'deleted' : 'created';
|
||||
return {
|
||||
state,
|
||||
isDeactivated: state !== 'created',
|
||||
deletionTime: data.deletion_time,
|
||||
};
|
||||
}
|
||||
|
||||
get permissionsPath() {
|
||||
return this.fullSecretPath || this.path;
|
||||
}
|
||||
|
||||
// permissions needed for the list view where kv/data has not yet been called. Allows us to conditionally show action items in the LinkedBlock popups.
|
||||
@lazyCapabilities(apiPath`${'backend'}/data/${'permissionsPath'}`, 'backend', 'permissionsPath') dataPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/metadata/${'permissionsPath'}`, 'backend', 'permissionsPath')
|
||||
metadataPath;
|
||||
|
||||
get canDeleteMetadata() {
|
||||
return this.metadataPath.get('canDelete') !== false;
|
||||
}
|
||||
get canReadMetadata() {
|
||||
return this.metadataPath.get('canRead') !== false;
|
||||
}
|
||||
get canUpdateMetadata() {
|
||||
return this.metadataPath.get('canUpdate') !== false;
|
||||
}
|
||||
get canCreateVersionData() {
|
||||
return this.dataPath.get('canUpdate') !== false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class KvDataSerializer extends ApplicationSerializer {
|
||||
serialize(snapshot) {
|
||||
const { secretData, casVersion } = snapshot.record;
|
||||
if (typeof casVersion === 'number') {
|
||||
/* if this is a number it is set by one of the following:
|
||||
A) user is creating initial version of a secret
|
||||
-> 0 : default value set in route
|
||||
B) user is creating a new version of a secret:
|
||||
-> metadata.current_version : has metadata read permissions (data permissions are irrelevant)
|
||||
-> secret.version : has data read permissions. without metadata read access a user is unable to navigate,
|
||||
to older secret versions so we assume creation is from the latest version */
|
||||
return { data: secretData, options: { cas: casVersion } };
|
||||
}
|
||||
// a non-number value means no read permission for both data and metadata
|
||||
return { data: secretData };
|
||||
}
|
||||
|
||||
normalizeKvData(payload) {
|
||||
const { data, metadata } = payload.data;
|
||||
return {
|
||||
...payload,
|
||||
data: {
|
||||
...payload.data,
|
||||
// Rename to secret_data so it doesn't get removed by normalizer
|
||||
secret_data: data,
|
||||
...metadata,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
if (requestType === 'queryRecord') {
|
||||
const transformed = this.normalizeKvData(payload);
|
||||
return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType);
|
||||
}
|
||||
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { assert } from '@ember/debug';
|
||||
import ApplicationSerializer from '../application';
|
||||
import { kvMetadataPath } from 'vault/utils/kv-path';
|
||||
|
||||
export default class KvMetadataSerializer extends ApplicationSerializer {
|
||||
attrs = {
|
||||
backend: { serialize: false },
|
||||
path: { serialize: false },
|
||||
oldestVersion: { serialize: false },
|
||||
createdTime: { serialize: false },
|
||||
updatedTime: { serialize: false },
|
||||
currentVersion: { serialize: false },
|
||||
versions: { serialize: false },
|
||||
};
|
||||
|
||||
normalizeItems(payload) {
|
||||
if (payload.data.keys) {
|
||||
assert('payload.backend must be provided on kv/metadata list response', !!payload.backend);
|
||||
return payload.data.keys.map((secret) => {
|
||||
// If there is no payload.path then we're either on a "top level" secret or the first level directory of a nested secret.
|
||||
// We set the path to the current secret or pathToSecret. e.g. my-secret or beep/boop/
|
||||
// We add a param called full_secret_path to the model which we use to navigate to the nested secret. e.g. beep/boop/bop.
|
||||
const fullSecretPath = payload.path ? payload.path + secret : secret;
|
||||
return {
|
||||
id: kvMetadataPath(payload.backend, fullSecretPath),
|
||||
path: secret,
|
||||
backend: payload.backend,
|
||||
full_secret_path: fullSecretPath,
|
||||
};
|
||||
});
|
||||
}
|
||||
return super.normalizeItems(payload);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,6 @@
|
|||
data-test-kv-suggestion-select
|
||||
as |secret|
|
||||
>
|
||||
{{secret.path}}
|
||||
{{secret}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
|
|
@ -10,9 +10,9 @@ import { action } from '@ember/object';
|
|||
import { guidFor } from '@ember/object/internals';
|
||||
import { run } from '@ember/runloop';
|
||||
import { keyIsFolder, parentKeyForKey, keyWithoutParentKey } from 'core/utils/key-utils';
|
||||
import { KvV2ListListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type KvSecretMetadataModel from 'vault/models/kv/metadata';
|
||||
import type ApiService from 'vault/services/api';
|
||||
|
||||
/**
|
||||
* @module KvSuggestionInput
|
||||
|
|
@ -49,12 +49,13 @@ interface PowerSelectAPI {
|
|||
}
|
||||
|
||||
export default class KvSuggestionInputComponent extends Component<Args> {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly api: ApiService;
|
||||
|
||||
@tracked secrets: KvSecretMetadataModel[] = [];
|
||||
@tracked secrets: string[] = [];
|
||||
powerSelectAPI: PowerSelectAPI | undefined;
|
||||
_cachedSecrets: KvSecretMetadataModel[] = []; // cache the response for filtering purposes
|
||||
_cachedSecrets: string[] = []; // cache the response for filtering purposes
|
||||
inputId = `suggestion-input-${guidFor(this)}`; // add unique segment to id in case multiple instances of component are used on the same page
|
||||
pathToSecret = ''; // keeps track of the full path to the secret as user builds it out
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
|
|
@ -63,18 +64,19 @@ export default class KvSuggestionInputComponent extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchSecrets(isDirectory: boolean) {
|
||||
const { mountPath } = this.args;
|
||||
get isDirectory() {
|
||||
return keyIsFolder(this.args.value);
|
||||
}
|
||||
|
||||
async fetchSecrets() {
|
||||
try {
|
||||
const { mountPath } = this.args;
|
||||
const backend = keyIsFolder(mountPath) ? mountPath.slice(0, -1) : mountPath;
|
||||
const parentDirectory = parentKeyForKey(this.args.value);
|
||||
const pathToSecret = isDirectory ? this.args.value : parentDirectory;
|
||||
const kvModels = (await this.store.query('kv/metadata', {
|
||||
backend,
|
||||
pathToSecret,
|
||||
})) as unknown;
|
||||
this.pathToSecret = this.isDirectory ? this.args.value : parentDirectory;
|
||||
const { keys } = await this.api.secrets.kvV2List(this.pathToSecret, backend, KvV2ListListEnum.TRUE);
|
||||
// this will be used to filter the existing result set when the search term changes within the same path
|
||||
this._cachedSecrets = kvModels as KvSecretMetadataModel[];
|
||||
this._cachedSecrets = keys || [];
|
||||
return this._cachedSecrets;
|
||||
} catch (error) {
|
||||
console.log(error); // eslint-disable-line
|
||||
|
|
@ -82,33 +84,32 @@ export default class KvSuggestionInputComponent extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
filterSecrets(kvModels: KvSecretMetadataModel[] | undefined = [], isDirectory: boolean) {
|
||||
filterSecrets(secrets: string[] | undefined = []) {
|
||||
const { value } = this.args;
|
||||
const secretName = keyWithoutParentKey(value) || '';
|
||||
return kvModels.filter((model) => {
|
||||
if (!value || isDirectory) {
|
||||
return secrets.filter((path) => {
|
||||
if (!value || this.isDirectory) {
|
||||
return true;
|
||||
}
|
||||
if (value === model.fullSecretPath) {
|
||||
if (secretName === path) {
|
||||
// don't show suggestion if it's currently selected
|
||||
return false;
|
||||
}
|
||||
return model.path.toLowerCase().includes(secretName.toLowerCase());
|
||||
return path.toLowerCase().includes(secretName.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateSuggestions() {
|
||||
const isFirstUpdate = !this._cachedSecrets.length;
|
||||
const isDirectory = keyIsFolder(this.args.value);
|
||||
if (!this.args.mountPath) {
|
||||
this.secrets = [];
|
||||
} else if (this.args.value && !isDirectory && this.secrets) {
|
||||
} else if (this.args.value && !this.isDirectory && this.secrets) {
|
||||
// if we don't need to fetch from a new path, filter the previous result set with the updated search term
|
||||
this.secrets = this.filterSecrets(this._cachedSecrets, isDirectory);
|
||||
this.secrets = this.filterSecrets(this._cachedSecrets);
|
||||
} else {
|
||||
const kvModels = await this.fetchSecrets(isDirectory);
|
||||
this.secrets = this.filterSecrets(kvModels, isDirectory);
|
||||
const secrets = await this.fetchSecrets();
|
||||
this.secrets = this.filterSecrets(secrets);
|
||||
}
|
||||
// don't do anything on first update -- allow dropdown to open on input click
|
||||
if (!isFirstUpdate) {
|
||||
|
|
@ -131,11 +132,11 @@ export default class KvSuggestionInputComponent extends Component<Args> {
|
|||
}
|
||||
|
||||
@action
|
||||
onSuggestionSelect(secret: KvSecretMetadataModel) {
|
||||
onSuggestionSelect(secret: string) {
|
||||
// user may partially type a value to filter result set and then select a suggestion
|
||||
// in this case the partially typed value must be replaced with suggestion value
|
||||
// the fullSecretPath contains the previous selections or typed path segments
|
||||
this.args.onChange(secret.fullSecretPath);
|
||||
// pathToSecret contains the previous selections or typed path segments
|
||||
this.args.onChange(`${this.pathToSecret}${secret}`);
|
||||
this.updateSuggestions();
|
||||
// refocus the input after selection
|
||||
run(() => document.getElementById(this.inputId)?.focus());
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
|
|||
|
||||
/**
|
||||
* @module List
|
||||
* ListPage component is a component to show a list of kv/metadata secrets.
|
||||
* ListPage component is a component to show a list of secrets.
|
||||
*
|
||||
* @param {array} secrets - An array of models generated form kv/metadata query.
|
||||
* @param {array} secrets - An array of secrets
|
||||
* @param {string} backend - The name of the kv secret engine.
|
||||
* @param {string} pathToSecret - The directory name that the secret belongs to ex: beep/boop/
|
||||
* @param {string} filterValue - The concatenation of the pathToSecret and pageFilter ex: beep/boop/my-
|
||||
|
|
|
|||
|
|
@ -56,51 +56,53 @@
|
|||
@title="You do not have access to read custom metadata"
|
||||
@message="In order to read custom metadata you either need read access to the secret data and/or read access to metadata."
|
||||
/>
|
||||
{{else if this.canRequestData}}
|
||||
{{else}}
|
||||
{{! Offer opportunity to manually request /data/ for custom_metadata }}
|
||||
{{#if this.error.isControlGroup}}
|
||||
<ControlGroupInlineError @error={{this.error}} class="has-top-margin-s has-bottom-margin-s" />
|
||||
{{else if this.error}}
|
||||
<MessageError @errorMessage={{this.error}} />
|
||||
{{/if}}
|
||||
<EmptyState
|
||||
@title="Request custom metadata?"
|
||||
@bottomBorder={{true}}
|
||||
@message="You do not have access to the metadata endpoint but you can retrieve custom metadata from the secret data endpoint."
|
||||
>
|
||||
<div class="is-block">
|
||||
<Hds::Alert @type="compact" @color="critical" class="has-top-margin-xs" as |A|>
|
||||
<A.Description>
|
||||
Sensitive secret data will be retrieved.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
<Hds::Button
|
||||
class="has-top-margin-xs"
|
||||
@text="Request data"
|
||||
@icon="reload"
|
||||
@iconPosition="trailing"
|
||||
@isFullWidth={{true}}
|
||||
data-test-request-data
|
||||
{{on "click" this.requestData}}
|
||||
/>
|
||||
</div>
|
||||
</EmptyState>
|
||||
{{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 @capabilities.canUpdateMetadata}}
|
||||
<Hds::Link::Standalone
|
||||
@icon="plus"
|
||||
@text="Add metadata"
|
||||
@route="secret.metadata.edit"
|
||||
@models={{array @backend @path}}
|
||||
data-test-add-custom-metadata
|
||||
/>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{#if this.canRequestData}}
|
||||
<EmptyState
|
||||
@title="Request custom metadata?"
|
||||
@bottomBorder={{true}}
|
||||
@message="You do not have access to the metadata endpoint but you can retrieve custom metadata from the secret data endpoint."
|
||||
>
|
||||
<div class="is-block">
|
||||
<Hds::Alert @type="compact" @color="critical" class="has-top-margin-xs" as |A|>
|
||||
<A.Description>
|
||||
Sensitive secret data will be retrieved.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
<Hds::Button
|
||||
class="has-top-margin-xs"
|
||||
@text="Request data"
|
||||
@icon="reload"
|
||||
@iconPosition="trailing"
|
||||
@isFullWidth={{true}}
|
||||
data-test-request-data
|
||||
{{on "click" this.requestData}}
|
||||
/>
|
||||
</div>
|
||||
</EmptyState>
|
||||
{{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 @capabilities.canUpdateMetadata}}
|
||||
<Hds::Link::Standalone
|
||||
@icon="plus"
|
||||
@text="Add metadata"
|
||||
@route="secret.metadata.edit"
|
||||
@models={{array @backend @path}}
|
||||
data-test-add-custom-metadata
|
||||
/>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import { action } from '@ember/object';
|
|||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* @module KvSecretMetadataDetails renders the details view for kv/metadata and button to delete (which deletes the whole secret) or edit metadata.
|
||||
* @module KvSecretMetadataDetails renders the details view for kv metadata and button to delete (which deletes the whole secret) or edit metadata.
|
||||
* <Page::Secret::Metadata::Details
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
|
|
@ -63,6 +64,7 @@ export default class KvSecretMetadataDetails extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
@waitFor
|
||||
async requestData() {
|
||||
const { backend, path } = this.args;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export default class KvEngine extends Engine {
|
|||
'namespace',
|
||||
'app-router',
|
||||
'secret-mount-path',
|
||||
'store',
|
||||
'pagination',
|
||||
'version',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { setupApplicationTest } from 'vault/tests/helpers';
|
|||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
||||
import { clearRecords, writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { grantAccessForWrite, setupControlGroup } from 'vault/tests/helpers/control-groups';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
|
@ -40,9 +40,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
|||
module('admin persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(tokenWithPolicyCmd(`admin-${this.backend}`, personas.admin(this.backend)));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cancel on create clears model (a)', async function (assert) {
|
||||
const backend = this.backend;
|
||||
|
|
@ -346,9 +344,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
|||
const token = await runCmd(
|
||||
tokenWithPolicyCmd(`data-reader-${this.backend}`, personas.dataReader(this.backend))
|
||||
);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cancel on create clears model (dr)', async function (assert) {
|
||||
const backend = this.backend;
|
||||
|
|
@ -492,9 +488,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
|||
const token = await runCmd(
|
||||
tokenWithPolicyCmd(`data-list-reader-${this.backend}`, personas.dataListReader(this.backend))
|
||||
);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cancel on create clears model (dlr)', async function (assert) {
|
||||
const backend = this.backend;
|
||||
|
|
@ -641,9 +635,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
|||
const token = await runCmd(
|
||||
tokenWithPolicyCmd(`data-list-reader-${this.backend}`, personas.metadataMaintainer(this.backend))
|
||||
);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cancel on create clears model (mm)', async function (assert) {
|
||||
const backend = this.backend;
|
||||
|
|
@ -838,9 +830,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
|
|||
const token = await runCmd(
|
||||
tokenWithPolicyCmd(`secret-creator-${this.backend}`, personas.secretCreator(this.backend))
|
||||
);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cancel on create clears model (sc)', async function (assert) {
|
||||
const backend = this.backend;
|
||||
|
|
@ -1090,9 +1080,7 @@ path "${this.backend}/metadata/*" {
|
|||
backend: this.backend,
|
||||
});
|
||||
this.userToken = userToken;
|
||||
await login(userToken);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(userToken);
|
||||
});
|
||||
test('create & update root secret with default metadata (cg)', async function (assert) {
|
||||
const backend = this.backend;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { setupApplicationTest } from 'vault/tests/helpers';
|
|||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
||||
import { clearRecords, deleteLatestCmd, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { deleteLatestCmd, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { setupControlGroup } from 'vault/tests/helpers/control-groups';
|
||||
import { click, currentRouteName, currentURL, waitUntil, visit } from '@ember/test-helpers';
|
||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
|
|
@ -63,9 +63,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
|
|||
// patch actions exist before/after deletion can run on both CE and ent repos
|
||||
this.version = this.owner.lookup('service:version').type = 'enterprise';
|
||||
const token = await runCmd(makeToken('admin', this.backend, personas.admin));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('can delete and undelete the latest secret version (a)', async function (assert) {
|
||||
assert.expect(21);
|
||||
|
|
@ -194,9 +192,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
|
|||
await runCmd(deleteLatestCmd(this.backend, 'nuke'));
|
||||
// login as data-reader persona
|
||||
const token = await runCmd(makeToken('data-reader', this.backend, personas.dataReader));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cannot delete and undelete the latest secret version (dr)', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
|
@ -246,9 +242,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
|
|||
await runCmd(deleteLatestCmd(this.backend, 'nuke'));
|
||||
// login as data-list-reader persona
|
||||
const token = await runCmd(makeToken('data-list-reader', this.backend, personas.dataListReader));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('can delete and cannot undelete the latest secret version (dlr)', async function (assert) {
|
||||
assert.expect(12);
|
||||
|
|
@ -310,9 +304,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
|
|||
await runCmd(deleteLatestCmd(this.backend, 'nuke'));
|
||||
// login as metadata-maintainer persona
|
||||
const token = await runCmd(makeToken('metadata-maintainer', this.backend, personas.metadataMaintainer));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cannot delete but can undelete the latest secret version (mm)', async function (assert) {
|
||||
assert.expect(18);
|
||||
|
|
@ -409,9 +401,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
|
|||
const token = await runCmd(
|
||||
makeToken('secret-nested-creator', this.backend, personas.secretNestedCreator)
|
||||
);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('can delete all secret versions from the nested list view (snc)', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
|
@ -434,9 +424,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook
|
|||
module('secret-creator persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(makeToken('secret-creator', this.backend, personas.secretCreator));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('cannot delete and undelete the latest secret version (sc)', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
|
@ -520,9 +508,7 @@ path "sys/control-group/request" {
|
|||
|
||||
const { userToken } = await setupControlGroup({ userPolicy, backend: this.backend });
|
||||
this.userToken = userToken;
|
||||
await login(userToken);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(userToken);
|
||||
});
|
||||
// Copy test outline from admin persona
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import {
|
|||
metadataListPolicy,
|
||||
metadataPolicy,
|
||||
} from 'vault/tests/helpers/kv/policy-generator';
|
||||
import { clearRecords, writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
|
|
@ -429,9 +429,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
|
|||
),
|
||||
createTokenCmd(`secret-patcher-${this.backend}`),
|
||||
]);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
|
||||
test('it patches a secret from the overview page', async function (assert) {
|
||||
|
|
@ -560,9 +558,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks)
|
|||
hooks.beforeEach(async function () {
|
||||
await loginNs(this.namespace);
|
||||
// mount engine within namespace
|
||||
await runCmd(mountEngineCmd('kv-v2', this.backend), false);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return runCmd(mountEngineCmd('kv-v2', this.backend), false);
|
||||
});
|
||||
hooks.afterEach(async function () {
|
||||
// visit logout with namespace query param because we're transitioning from within an engine
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {
|
|||
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
||||
import {
|
||||
addSecretMetadataCmd,
|
||||
clearRecords,
|
||||
writeSecret,
|
||||
writeVersionedSecret,
|
||||
} from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
|
|
@ -242,9 +241,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
|||
const token = await runCmd(
|
||||
tokenWithPolicyCmd('admin', personas.admin(this.backend) + personas.admin(this.emptyBackend))
|
||||
);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('empty backend - breadcrumbs, title, tabs, emptyState (a)', async function (assert) {
|
||||
assert.expect(23);
|
||||
|
|
@ -599,9 +596,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
|||
),
|
||||
createTokenCmd(`data-reader-${this.backend}`),
|
||||
]);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('empty backend - breadcrumbs, title, tabs, emptyState (dr)', async function (assert) {
|
||||
assert.expect(16);
|
||||
|
|
@ -792,9 +787,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
|||
createTokenCmd(`data-reader-list-${this.backend}`),
|
||||
]);
|
||||
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('empty backend - breadcrumbs, title, tabs, emptyState (dlr)', async function (assert) {
|
||||
assert.expect(15);
|
||||
|
|
@ -985,9 +978,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
|||
),
|
||||
createTokenCmd(`metadata-maintainer-${this.backend}`),
|
||||
]);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('empty backend - breadcrumbs, title, tabs, emptyState (mm)', async function (assert) {
|
||||
assert.expect(15);
|
||||
|
|
@ -1208,9 +1199,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
|
|||
),
|
||||
createTokenCmd(`secret-creator-${this.backend}`),
|
||||
]);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
test('empty backend - breadcrumbs, title, tabs, emptyState (sc)', async function (assert) {
|
||||
assert.expect(15);
|
||||
|
|
@ -1444,9 +1433,7 @@ path "${this.backend}/subkeys/*" {
|
|||
`;
|
||||
const { userToken } = await setupControlGroup({ userPolicy, backend: this.backend });
|
||||
this.userToken = userToken;
|
||||
await login(userToken);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(userToken);
|
||||
});
|
||||
test('can access nested secret (cg)', async function (assert) {
|
||||
assert.expect(44);
|
||||
|
|
@ -1695,9 +1682,7 @@ path "${this.backend}/subkeys/*" {
|
|||
),
|
||||
createTokenCmd(`secret-patcher-${this.backend}`),
|
||||
]);
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return;
|
||||
return login(token);
|
||||
});
|
||||
|
||||
test('it navigates to patch a secret from overview', async function (assert) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
|||
import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
||||
import {
|
||||
clearRecords,
|
||||
deleteVersionCmd,
|
||||
destroyVersionCmd,
|
||||
writeVersionedSecret,
|
||||
|
|
@ -56,8 +55,7 @@ module('Acceptance | kv-v2 workflow | version history, paths', function (hooks)
|
|||
module('admin persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(makeToken('admin', this.backend, personas.admin));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return login(token);
|
||||
});
|
||||
test('can navigate to the version history page (a)', async function (assert) {
|
||||
await this.navToSecret();
|
||||
|
|
@ -105,8 +103,7 @@ module('Acceptance | kv-v2 workflow | version history, paths', function (hooks)
|
|||
module('data-reader persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(makeToken('data-reader', this.backend, personas.dataReader));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return login(token);
|
||||
});
|
||||
test('cannot navigate to the version history page (dr)', async function (assert) {
|
||||
await this.navToSecret();
|
||||
|
|
@ -132,8 +129,7 @@ module('Acceptance | kv-v2 workflow | version history, paths', function (hooks)
|
|||
module('data-list-reader persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(makeToken('data-list-reader', this.backend, personas.dataListReader));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return login(token);
|
||||
});
|
||||
test('cannot navigate to the version history page (dlr)', async function (assert) {
|
||||
await this.navToSecret();
|
||||
|
|
@ -159,8 +155,7 @@ module('Acceptance | kv-v2 workflow | version history, paths', function (hooks)
|
|||
module('metadata-maintainer persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(makeToken('metadata-maintainer', this.backend, personas.metadataMaintainer));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return login(token);
|
||||
});
|
||||
test('can navigate to the version history page (mm)', async function (assert) {
|
||||
await this.navToSecret();
|
||||
|
|
@ -208,8 +203,7 @@ module('Acceptance | kv-v2 workflow | version history, paths', function (hooks)
|
|||
module('secret-creator persona', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const token = await runCmd(makeToken('secret-creator', this.backend, personas.secretCreator));
|
||||
await login(token);
|
||||
clearRecords(this.store);
|
||||
return login(token);
|
||||
});
|
||||
test('cannot navigate to the version history page (sc)', async function (assert) {
|
||||
await this.navToSecret();
|
||||
|
|
@ -255,8 +249,7 @@ path "${this.backend}/*" {
|
|||
`;
|
||||
const { userToken } = await setupControlGroup({ userPolicy, backend: this.backend });
|
||||
this.userToken = userToken;
|
||||
await login(userToken);
|
||||
clearRecords(this.store);
|
||||
return login(userToken);
|
||||
});
|
||||
test('can navigate to the version history page (cg)', async function (assert) {
|
||||
await this.navToSecret();
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@
|
|||
import { click, fillIn, visit, settled } from '@ember/test-helpers';
|
||||
import { FORM } from './kv-selectors';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { assert } from '@ember/debug';
|
||||
import { kvMetadataPath } from 'vault/utils/kv-path';
|
||||
|
||||
// CUSTOM ACTIONS RELEVANT TO KV-V2
|
||||
|
||||
|
|
@ -61,38 +58,3 @@ export const addSecretMetadataCmd = (backend, secret, options = { max_versions:
|
|||
}, '');
|
||||
return `write ${backend}/metadata/${secret} ${stringOptions}`;
|
||||
};
|
||||
|
||||
// Clears kv-related data and capabilities so that admin
|
||||
// capabilities from setup don't rollover
|
||||
export function clearRecords(store) {
|
||||
store.unloadAll('kv/data');
|
||||
store.unloadAll('kv/metatata');
|
||||
store.unloadAll('capabilities');
|
||||
}
|
||||
|
||||
// TEST SETUP HELPERS
|
||||
|
||||
// sets basic path, backend, and metadata
|
||||
export const baseSetup = (context) => {
|
||||
assert(
|
||||
`'baseSetup()' requires mirage: import { setupMirage } from 'ember-cli-mirage/test-support'`,
|
||||
context.server
|
||||
);
|
||||
context.store = context.owner.lookup('service:store');
|
||||
context.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
context.backend = 'kv-engine';
|
||||
context.path = 'my-secret';
|
||||
context.metadata = metadataModel(context, { withCustom: false });
|
||||
};
|
||||
|
||||
export const metadataModel = (context, { withCustom = false }) => {
|
||||
const metadata = withCustom
|
||||
? context.server.create('kv-metadatum', 'withCustomMetadata')
|
||||
: context.server.create('kv-metadatum');
|
||||
metadata.id = kvMetadataPath(context.backend, context.path);
|
||||
context.store.pushPayload('kv/metadata', {
|
||||
modelName: 'kv/metadata',
|
||||
...metadata,
|
||||
});
|
||||
return context.store.peekRecord('kv/metadata', metadata.id);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render, click, fillIn, settled, typeIn } from '@ember/test-helpers';
|
||||
import { render, click, fillIn, typeIn } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const {
|
||||
searchSelect: { option, options },
|
||||
|
|
@ -17,16 +17,16 @@ const {
|
|||
|
||||
module('Integration | Component | kv-suggestion-input', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.label = 'Input something';
|
||||
this.mountPath = 'secret/';
|
||||
this.keys = ['foo/', 'my-secret'];
|
||||
const response = () => ({ data: { keys: this.keys } });
|
||||
this.server.get('/:mount/metadata', response);
|
||||
this.server.get('/:mount/metadata/*', response);
|
||||
return render(hbs`
|
||||
this.apiStub = sinon
|
||||
.stub(this.owner.lookup('service:api').secrets, 'kvV2List')
|
||||
.resolves({ keys: ['foo/', 'my-secret'] });
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(hbs`
|
||||
<KvSuggestionInput
|
||||
@label={{this.label}}
|
||||
@subText="Suggestions will display"
|
||||
|
|
@ -38,19 +38,23 @@ module('Integration | Component | kv-suggestion-input', function (hooks) {
|
|||
});
|
||||
|
||||
test('it should render label and sub text', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('label').hasText('Input something', 'Label renders');
|
||||
assert.dom('[data-test-label-subtext]').hasText('Suggestions will display', 'Label subtext renders');
|
||||
this.set('label', null);
|
||||
await settled();
|
||||
|
||||
this.label = null;
|
||||
await this.renderComponent();
|
||||
assert.dom('label').doesNotExist('Label is hidden when arg is not provided');
|
||||
});
|
||||
|
||||
test('it should disable input when mountPath is not provided', async function (assert) {
|
||||
this.set('mountPath', null);
|
||||
this.mountPath = null;
|
||||
await this.renderComponent();
|
||||
assert.dom(input).isDisabled('Input is disabled when mountPath is not provided');
|
||||
});
|
||||
|
||||
test('it should fetch suggestions for initial mount path', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(options).doesNotExist('Suggestions are hidden initially');
|
||||
await click(input);
|
||||
['foo/', 'my-secret'].forEach((secret, index) => {
|
||||
|
|
@ -59,14 +63,15 @@ module('Integration | Component | kv-suggestion-input', function (hooks) {
|
|||
});
|
||||
|
||||
test('it should fetch secrets and update suggestions on mountPath change', async function (assert) {
|
||||
this.keys = ['test1'];
|
||||
this.set('mountPath', 'foo/');
|
||||
await settled();
|
||||
this.apiStub.resolves({ keys: ['test1'] });
|
||||
this.mountPath = 'foo/';
|
||||
await this.renderComponent();
|
||||
await click(input);
|
||||
assert.dom(option()).hasText('test1', 'Suggestions are fetched and render on mountPath change');
|
||||
});
|
||||
|
||||
test('it should filter current result set', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(input);
|
||||
await typeIn(input, 'sec');
|
||||
assert.dom(options).exists({ count: 1 }, 'Correct number of options render based on input value');
|
||||
|
|
@ -74,12 +79,13 @@ module('Integration | Component | kv-suggestion-input', function (hooks) {
|
|||
});
|
||||
|
||||
test('it should replace filter terms with full path to secret', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillIn(input, 'sec');
|
||||
await click(option());
|
||||
assert.dom(input).hasValue('my-secret', 'Partial term replaced with selected secret');
|
||||
|
||||
await fillIn(input, '');
|
||||
this.keys = ['secret-nested', 'bar', 'baz'];
|
||||
this.apiStub.resolves({ keys: ['secret-nested', 'bar', 'baz'] });
|
||||
await click(option());
|
||||
await fillIn(input, 'nest');
|
||||
await click(option());
|
||||
|
|
@ -89,23 +95,25 @@ module('Integration | Component | kv-suggestion-input', function (hooks) {
|
|||
});
|
||||
|
||||
test('it should fetch secrets at nested paths', async function (assert) {
|
||||
this.keys = ['bar/'];
|
||||
await this.renderComponent();
|
||||
this.apiStub.resolves({ keys: ['bar/'] });
|
||||
await click(input);
|
||||
await click(option());
|
||||
assert.dom(input).hasValue('foo/', 'Input value updates on select');
|
||||
assert.dom(option()).hasText('bar/', 'Suggestions are fetched at new path');
|
||||
|
||||
this.keys = ['baz/'];
|
||||
this.apiStub.resolves({ keys: ['baz/'] });
|
||||
await click(option());
|
||||
assert.dom(input).hasValue('foo/bar/', 'Input value updates on select');
|
||||
assert.dom(option()).hasText('baz/', 'Suggestions are fetched at new path');
|
||||
|
||||
this.keys = ['nested-secret'];
|
||||
this.apiStub.resolves({ keys: ['nested-secret'] });
|
||||
await typeIn(input, 'baz/');
|
||||
assert.dom(option()).hasText('nested-secret', 'Suggestions are fetched at new path');
|
||||
});
|
||||
|
||||
test('it should only render dropdown when suggestions exist', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(input);
|
||||
assert.dom(options).exists({ count: 2 }, 'Suggestions render');
|
||||
|
||||
|
|
@ -115,7 +123,7 @@ module('Integration | Component | kv-suggestion-input', function (hooks) {
|
|||
await fillIn(input, '');
|
||||
assert.dom(options).exists({ count: 2 }, 'Suggestions render');
|
||||
|
||||
this.keys = [];
|
||||
this.apiStub.resolves({ keys: [] });
|
||||
await click(option());
|
||||
assert.dom(options).doesNotExist('Drop down is hidden when there are no suggestions');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,46 +6,54 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { kvDataPath } from 'vault/utils/kv-path';
|
||||
|
||||
module('Integration | Component | kv | kv-page-header', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.backend = 'kv-engine';
|
||||
this.path = 'my-secret';
|
||||
this.version = 2;
|
||||
this.id = kvDataPath(this.backend, this.path, this.version);
|
||||
this.payload = {
|
||||
backend: this.backend,
|
||||
path: this.path,
|
||||
version: 2,
|
||||
};
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
id: this.id,
|
||||
...this.payload,
|
||||
});
|
||||
|
||||
this.model = this.store.peekRecord('kv/data', this.id);
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.model.backend, route: 'secrets' },
|
||||
{ label: this.model.path, route: 'secrets.secret.details', model: this.model.path },
|
||||
{ label: this.backend, route: 'secrets' },
|
||||
{ label: this.path, route: 'secrets.secret.details', model: this.path },
|
||||
{ label: 'Edit' },
|
||||
];
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`
|
||||
<KvPageHeader
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@pageTitle={{this.pageTitle}}
|
||||
@mountName={{this.mountName}}
|
||||
@secretPath={{this.secretPath}}
|
||||
>
|
||||
<:tabLinks>
|
||||
<li><LinkTo @route="list" data-test-secrets-tab="Secrets">Secrets</LinkTo></li>
|
||||
<li><LinkTo @route="configuration" data-test-secrets-tab="Configuration">Configuration</LinkTo></li>
|
||||
</:tabLinks>
|
||||
|
||||
<:toolbarActions>
|
||||
<ToolbarLink @route="secrets.create" @type="add">Create secret</ToolbarLink>
|
||||
</:toolbarActions>
|
||||
|
||||
<:toolbarFilters>
|
||||
<p>stuff here</p>
|
||||
</:toolbarFilters>
|
||||
</KvPageHeader>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders breadcrumbs', async function (assert) {
|
||||
assert.expect(4);
|
||||
await render(hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @pageTitle="Create new version"/>`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-breadcrumbs] li:nth-child(1) a').hasText('Secrets', 'Secrets breadcrumb renders');
|
||||
assert.dom('[data-test-breadcrumbs] li:nth-child(2) a').hasText(this.backend, 'engine name renders');
|
||||
assert.dom('[data-test-breadcrumbs] li:nth-child(3) a').hasText(this.path, 'secret path renders');
|
||||
|
|
@ -56,17 +64,16 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
|
|||
|
||||
test('it renders a custom title for @pageTitle', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @pageTitle="Create new version"/>`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
this.pageTitle = 'Create new version';
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-header-title]').hasText('Create new version', 'displays custom title.');
|
||||
assert.dom('[data-test-header-title] svg').doesNotExist('Does not show icon if not at engine level.');
|
||||
});
|
||||
|
||||
test('it renders a title and copy button for @secretPath', async function (assert) {
|
||||
await render(hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @secretPath="my/secret/path"/>`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
assert.expect(3);
|
||||
this.secretPath = 'my/secret/path';
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-header-title]').hasText('my/secret/path', 'displays path');
|
||||
assert.dom('[data-test-header-title] button').exists('renders copy button for path');
|
||||
assert.dom('[data-test-icon="clipboard-copy"]').exists('renders copy icon');
|
||||
|
|
@ -74,9 +81,8 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
|
|||
|
||||
test('it renders a title, icon and tag if engine view', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @mountName={{this.backend}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
this.mountName = this.backend;
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom('[data-test-header-title]')
|
||||
.hasText(`${this.backend} version 2`, 'Mount path and version tag render for title.');
|
||||
|
|
@ -87,18 +93,7 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
|
|||
|
||||
test('it renders tabs', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(
|
||||
hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @mountName="my-engine">
|
||||
<:tabLinks>
|
||||
<li><LinkTo @route="list" data-test-secrets-tab="Secrets">Secrets</LinkTo></li>
|
||||
<li><LinkTo @route="configuration" data-test-secrets-tab="Configuration">Configuration</LinkTo></li>
|
||||
</:tabLinks>
|
||||
</KvPageHeader>
|
||||
`,
|
||||
{
|
||||
owner: this.engine,
|
||||
}
|
||||
);
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-secrets-tab="Secrets"]').hasText('Secrets', 'Secrets tab renders');
|
||||
assert
|
||||
.dom('[data-test-secrets-tab="Configuration"]')
|
||||
|
|
@ -107,29 +102,13 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
|
|||
|
||||
test('it should yield block for toolbar actions', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(
|
||||
hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @mountName="my-engine">
|
||||
<:toolbarActions>
|
||||
<ToolbarLink @route="secrets.create" @type="add">Create secret</ToolbarLink>
|
||||
</:toolbarActions>
|
||||
</KvPageHeader>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
),
|
||||
assert.dom('.toolbar-actions').exists('Block is yielded for toolbar actions');
|
||||
await this.renderComponent();
|
||||
assert.dom('.toolbar-actions').exists('Block is yielded for toolbar actions');
|
||||
});
|
||||
|
||||
test('it should yield block for toolbar filters', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(
|
||||
hbs`<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @mountName="my-engine">
|
||||
<:toolbarFilters>
|
||||
<p>stuff here</p>
|
||||
</:toolbarFilters>
|
||||
</KvPageHeader>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
),
|
||||
assert.dom('.toolbar-filters').exists('Block is yielded for toolbar filters');
|
||||
await this.renderComponent();
|
||||
assert.dom('.toolbar-filters').exists('Block is yielded for toolbar filters');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
|||
import { selectChoose } from 'ember-power-select/test-support';
|
||||
import { Response } from 'miragejs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const { destinations, searchSelect, messageError, kvSuggestion } = PAGE;
|
||||
const { mountSelect, mountInput, successMessage } = destinations.sync;
|
||||
|
|
@ -25,15 +26,12 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
|
|||
setupDataStubs(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.server.get('/sys/internal/ui/mounts', () => ({
|
||||
data: { secret: { 'my-kv/': { type: 'kv', options: { version: '2' } } } },
|
||||
}));
|
||||
this.server.get('/my-kv/metadata', () => ({
|
||||
data: { keys: ['my-path/', 'my-secret'] },
|
||||
}));
|
||||
this.server.get('/my-kv/metadata/my-path', () => ({
|
||||
data: { keys: ['nested-secret'] },
|
||||
}));
|
||||
const api = this.owner.lookup('service:api');
|
||||
this.mountsStub = sinon
|
||||
.stub(api.sys, 'internalUiListEnabledVisibleMounts')
|
||||
.resolves({ secret: { 'my-kv/': { type: 'kv', options: { version: '2' } } } });
|
||||
|
||||
this.secretsStub = sinon.stub(api.secrets, 'kvV2List').resolves({ keys: ['my-path/', 'my-secret'] });
|
||||
|
||||
await render(
|
||||
hbs`<Secrets::Page::Destinations::Destination::Sync @destination={{this.destination}} @capabilities={{this.capabilities}} />`,
|
||||
|
|
@ -62,6 +60,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
|
|||
|
||||
test('it should render secret suggestions for nested paths', async function (assert) {
|
||||
await selectChoose(mountSelect, '.ember-power-select-option', 1);
|
||||
this.secretsStub.resolves({ keys: ['nested-secret'] });
|
||||
await click(kvSuggestion.input);
|
||||
await click(searchSelect.option());
|
||||
assert
|
||||
|
|
@ -99,9 +98,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
|
|||
test('it should allow manual mount path input if kv mounts are not returned', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.get('/sys/internal/ui/mounts', () => ({
|
||||
data: { secret: { 'cubbyhole/': { type: 'cubbyhole' } } },
|
||||
}));
|
||||
this.mountsStub.resolves({ secret: { 'cubbyhole/': { type: 'cubbyhole' } } });
|
||||
|
||||
const { type, name } = this.destination;
|
||||
this.server.post(`/sys/sync/destinations/${type}/${name}/associations/set`, (schema, req) => {
|
||||
|
|
|
|||
|
|
@ -1,469 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { kvDataPath } from 'vault/utils/kv-path';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
const EXAMPLE_KV_DATA_CREATE_RESPONSE = {
|
||||
request_id: 'foobar',
|
||||
data: {
|
||||
created_time: '2023-06-21T16:18:31.479993Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const EXAMPLE_KV_DATA_GET_RESPONSE = {
|
||||
request_id: 'foobar',
|
||||
data: {
|
||||
data: { foo: 'bar' },
|
||||
metadata: {
|
||||
created_time: '2023-06-20T21:26:47.592306Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const EXAMPLE_KV_SUBKEYS_RESPONSE = {
|
||||
request_id: 'foobar',
|
||||
data: {
|
||||
...EXAMPLE_KV_DATA_GET_RESPONSE,
|
||||
data: { foo: null },
|
||||
},
|
||||
};
|
||||
|
||||
const EXAMPLE_CONTROL_GROUP_RESPONSE = {
|
||||
data: null,
|
||||
wrap_info: {
|
||||
token: 'some-token',
|
||||
accessor: 'some-accessor',
|
||||
ttl: 86400,
|
||||
creation_time: '2023-08-09T16:08:06-05:00',
|
||||
creation_path: 'some/path/here',
|
||||
},
|
||||
};
|
||||
|
||||
const EXAMPLE_KV_DATA_DESTROYED = {
|
||||
data: {
|
||||
data: null,
|
||||
metadata: {
|
||||
created_time: '2023-08-09T20:10:24.4825Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: true,
|
||||
version: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const EXAMPLE_KV_DATA_DELETED = {
|
||||
data: {
|
||||
data: null,
|
||||
metadata: {
|
||||
created_time: '2023-08-09T20:10:24.571332Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '2023-08-09T20:10:24.70176Z',
|
||||
destroyed: false,
|
||||
version: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module('Unit | Adapter | kv/data', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.backend = 'my/kv-back&end';
|
||||
this.secretMountPath.currentPath = this.backend;
|
||||
this.path = 'beep/bop/my secret';
|
||||
this.version = '2';
|
||||
this.id = kvDataPath(this.backend, this.path, this.version);
|
||||
this.data = {
|
||||
options: {
|
||||
cas: 2,
|
||||
},
|
||||
data: {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
this.payload = {
|
||||
backend: this.backend,
|
||||
path: this.path,
|
||||
version: 2,
|
||||
};
|
||||
this.endpoint = (noun) => `${encodePath(this.backend)}/${noun}/${encodePath(this.path)}`;
|
||||
});
|
||||
|
||||
module('createRecord', function () {
|
||||
test('it should make request to correct endpoint on createRecord', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.server.post(this.endpoint('data'), (schema, req) => {
|
||||
assert.ok('POST request made to correct endpoint when creating new record');
|
||||
const body = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
data: {
|
||||
foo: 'bar',
|
||||
},
|
||||
options: {
|
||||
cas: 0,
|
||||
},
|
||||
});
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
const record = this.store.createRecord('kv/data', {
|
||||
backend: this.backend,
|
||||
path: this.path,
|
||||
secretData: { foo: 'bar' },
|
||||
casVersion: 0,
|
||||
});
|
||||
await record.save();
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.version, 1, 'record has correct version');
|
||||
assert.deepEqual(record.secretData, { foo: 'bar' }, 'record has correct data');
|
||||
assert.strictEqual(record.createdTime, '2023-06-21T16:18:31.479993Z', 'record has correct createdTime');
|
||||
assert.strictEqual(
|
||||
record.id,
|
||||
`${encodePath(this.backend)}/data/${encodePath(this.path)}?version=1`,
|
||||
'record has correct id'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should not send cas if casVersion is not a number', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.server.post(this.endpoint('data'), (schema, req) => {
|
||||
assert.ok('POST request made to correct endpoint when creating new record');
|
||||
const body = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
data: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
const record = this.store.createRecord('kv/data', {
|
||||
backend: this.backend,
|
||||
path: this.path,
|
||||
secretData: { foo: 'bar' },
|
||||
});
|
||||
await record.save();
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.version, 1, 'record has correct version');
|
||||
assert.deepEqual(record.secretData, { foo: 'bar' }, 'record has correct data');
|
||||
assert.strictEqual(record.createdTime, '2023-06-21T16:18:31.479993Z', 'record has correct createdTime');
|
||||
assert.strictEqual(
|
||||
record.id,
|
||||
`${encodePath(this.backend)}/data/${encodePath(this.path)}?version=1`,
|
||||
'record has correct id'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('queryRecord', function () {
|
||||
test('it should make request to correct endpoint on queryRecord', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.server.get(this.endpoint('data'), (schema, req) => {
|
||||
assert.ok(true, 'request is made to correct url on queryRecord.');
|
||||
assert.strictEqual(
|
||||
req.queryParams.version,
|
||||
this.version,
|
||||
'request includes the version flag on queryRecord.'
|
||||
);
|
||||
return EXAMPLE_KV_DATA_GET_RESPONSE;
|
||||
});
|
||||
|
||||
const record = await this.store.queryRecord('kv/data', this.payload);
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.version, 2, 'record has correct version');
|
||||
assert.deepEqual(record.secretData, { foo: 'bar' }, 'record has correct data');
|
||||
assert.strictEqual(record.createdTime, '2023-06-20T21:26:47.592306Z', 'record has correct createdTime');
|
||||
assert.strictEqual(
|
||||
record.id,
|
||||
`${encodePath(this.backend)}/data/${encodePath(this.path)}?version=${this.version}`,
|
||||
'record has correct id'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should handle a 404 not found response properly', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get(this.endpoint('data'), () => {
|
||||
// This is what the API currently returns for not found
|
||||
return new Response(404, {}, { errors: [] });
|
||||
});
|
||||
|
||||
try {
|
||||
await this.store.queryRecord('kv/data', this.payload);
|
||||
} catch (e) {
|
||||
assert.ok('throws the error');
|
||||
}
|
||||
});
|
||||
|
||||
test('it should handle 404 for a soft-deleted version properly', async function (assert) {
|
||||
this.server.get(this.endpoint('data'), () => {
|
||||
return new Response(404, {}, EXAMPLE_KV_DATA_DELETED);
|
||||
});
|
||||
|
||||
const record = await this.store.queryRecord('kv/data', this.payload);
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.version, 2, 'record has version based on request');
|
||||
assert.strictEqual(record.deletionTime, '2023-08-09T20:10:24.70176Z', 'record includes deletion time');
|
||||
assert.strictEqual(record.failReadErrorCode, undefined, 'record does not have failed error code');
|
||||
assert.strictEqual(
|
||||
record.id,
|
||||
`${encodePath(this.backend)}/data/${encodePath(this.path)}?version=${this.version}`,
|
||||
'record has correct id'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should handle 404 for a destroyed version properly', async function (assert) {
|
||||
this.server.get(this.endpoint('data'), () => {
|
||||
return new Response(404, {}, EXAMPLE_KV_DATA_DESTROYED);
|
||||
});
|
||||
|
||||
const record = await this.store.queryRecord('kv/data', this.payload);
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.version, 2, 'record has version based on request');
|
||||
assert.true(record.destroyed, 'record has destroyed value');
|
||||
assert.strictEqual(record.failReadErrorCode, undefined, 'record does not have error code');
|
||||
assert.strictEqual(
|
||||
record.id,
|
||||
`${encodePath(this.backend)}/data/${encodePath(this.path)}?version=${this.version}`,
|
||||
'record has correct id'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should handle a 403 permission denied properly', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.server.get(this.endpoint('data'), (schema, req) => {
|
||||
assert.ok(true, 'request is made to correct url on queryRecord.');
|
||||
assert.strictEqual(
|
||||
req.queryParams.version,
|
||||
this.version,
|
||||
'request includes the version flag on queryRecord.'
|
||||
);
|
||||
return new Response(403, {}, { errors: ['1 error occurred:\n\t* permission denied\n\n'] });
|
||||
});
|
||||
|
||||
const record = await this.store.queryRecord('kv/data', this.payload);
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.version, 2, 'record has version based on request');
|
||||
assert.strictEqual(record.secretData, undefined, 'record does not include data');
|
||||
assert.strictEqual(record.failReadErrorCode, 403, 'record has error response recorded');
|
||||
assert.strictEqual(
|
||||
record.id,
|
||||
`${encodePath(this.backend)}/data/${encodePath(this.path)}?version=${this.version}`,
|
||||
'record has correct id'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should handle a control group response properly', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.owner.lookup('service:version').type = 'enterprise'; // Required for testing control-group flow
|
||||
this.server.get(this.endpoint('data'), () => {
|
||||
return EXAMPLE_CONTROL_GROUP_RESPONSE;
|
||||
});
|
||||
|
||||
try {
|
||||
await this.store.queryRecord('kv/data', this.payload);
|
||||
} catch (e) {
|
||||
assert.ok('throws the error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module('destroyRecord', function () {
|
||||
test('it should make request to correct endpoint on delete latest version', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.delete(this.endpoint('data'), () => {
|
||||
assert.ok(true, 'request made to correct endpoint on delete latest version.');
|
||||
return new Response(204);
|
||||
});
|
||||
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
id: this.id,
|
||||
...this.payload,
|
||||
});
|
||||
let record = await this.store.peekRecord('kv/data', this.id);
|
||||
await record.destroyRecord({ adapterOptions: { deleteType: 'delete-latest-version' } });
|
||||
assert.true(record.isDeleted, 'record is deleted');
|
||||
record = await this.store.peekRecord('kv/data', this.id);
|
||||
assert.strictEqual(record, null, 'record is no longer in store');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on delete specific versions', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.post(this.endpoint('delete'), (schema, req) => {
|
||||
const { versions } = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(versions, 2, 'version array is sent in the payload.');
|
||||
assert.ok(true, 'request made to correct endpoint on delete specific version.');
|
||||
});
|
||||
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
id: this.id,
|
||||
...this.payload,
|
||||
});
|
||||
let record = await this.store.peekRecord('kv/data', this.id);
|
||||
await record.destroyRecord({
|
||||
adapterOptions: { deleteType: 'delete-version', deleteVersions: 2 },
|
||||
});
|
||||
assert.true(record.isDeleted, 'record is deleted');
|
||||
record = await this.store.peekRecord('kv/data', this.id);
|
||||
assert.strictEqual(record, null, 'record is no longer in store');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on undelete', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.post(`${this.backend}/undelete/${this.path}`, (schema, req) => {
|
||||
const { versions } = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(versions, 2, 'version array is sent in the payload.');
|
||||
assert.ok(true, 'request made to correct endpoint on undelete specific version.');
|
||||
});
|
||||
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
id: this.id,
|
||||
...this.payload,
|
||||
});
|
||||
let record = await this.store.peekRecord('kv/data', this.id);
|
||||
|
||||
await record.destroyRecord({
|
||||
adapterOptions: { deleteType: 'undelete', deleteVersions: 2 },
|
||||
});
|
||||
assert.true(record.isDeleted, 'record is deleted');
|
||||
record = await this.store.peekRecord('kv/data', this.id);
|
||||
assert.strictEqual(record, null, 'record is no longer in store');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on destroy specific versions', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.put(`${encodePath(this.backend)}/destroy/${encodePath(this.path)}`, (schema, req) => {
|
||||
const { versions } = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(versions, 2, 'version array is sent in the payload.');
|
||||
assert.ok(true, 'request made to correct endpoint on destroy specific version.');
|
||||
});
|
||||
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
id: this.id,
|
||||
...this.payload,
|
||||
});
|
||||
let record = await this.store.peekRecord('kv/data', this.id);
|
||||
await record.destroyRecord({
|
||||
adapterOptions: { deleteType: 'destroy', deleteVersions: 2 },
|
||||
});
|
||||
assert.true(record.isDeleted, 'record is deleted');
|
||||
record = await this.store.peekRecord('kv/data', this.id);
|
||||
assert.strictEqual(record, null, 'record is no longer in store');
|
||||
});
|
||||
});
|
||||
|
||||
module('fetchSubkeys', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.adapter = this.store.adapterFor('kv/data');
|
||||
this.subkeysUrl = `${encodePath(this.backend)}/subkeys/${encodePath(this.path)}`;
|
||||
});
|
||||
|
||||
test('it should make request with no query', async function (assert) {
|
||||
assert.expect(2);
|
||||
const expectedQuery = {};
|
||||
this.server.get(this.subkeysUrl, (schema, { queryParams }) => {
|
||||
assert.true(true, `GET request made to ${this.subkeysUrl}`);
|
||||
assert.propEqual(queryParams, expectedQuery, `queryParams contain: ${JSON.stringify(queryParams)}`);
|
||||
return EXAMPLE_KV_SUBKEYS_RESPONSE;
|
||||
});
|
||||
|
||||
this.adapter.fetchSubkeys(this.backend, this.path);
|
||||
});
|
||||
|
||||
test('it should make request with version param', async function (assert) {
|
||||
assert.expect(1);
|
||||
const query = { version: '2' };
|
||||
this.server.get(this.subkeysUrl, (schema, { queryParams }) => {
|
||||
assert.propEqual(queryParams, query, `queryParams contain: ${JSON.stringify(queryParams)}`);
|
||||
return EXAMPLE_KV_SUBKEYS_RESPONSE;
|
||||
});
|
||||
|
||||
this.adapter.fetchSubkeys(this.backend, this.path, query);
|
||||
});
|
||||
|
||||
test('it should make request with depth param', async function (assert) {
|
||||
assert.expect(1);
|
||||
const query = { depth: '1' };
|
||||
this.server.get(this.subkeysUrl, (schema, { queryParams }) => {
|
||||
assert.propEqual(queryParams, query, `queryParams contain: ${JSON.stringify(queryParams)}`);
|
||||
return EXAMPLE_KV_SUBKEYS_RESPONSE;
|
||||
});
|
||||
|
||||
this.adapter.fetchSubkeys(this.backend, this.path, query);
|
||||
});
|
||||
|
||||
test('it should make request with both params as strings', async function (assert) {
|
||||
assert.expect(1);
|
||||
const query = { version: '2', depth: '1' };
|
||||
this.server.get(this.subkeysUrl, (schema, { queryParams }) => {
|
||||
assert.propEqual(queryParams, query, `queryParams contain: ${JSON.stringify(queryParams)}`);
|
||||
return EXAMPLE_KV_SUBKEYS_RESPONSE;
|
||||
});
|
||||
|
||||
this.adapter.fetchSubkeys(this.backend, this.path, query);
|
||||
});
|
||||
|
||||
test('it should make request with both params as integers', async function (assert) {
|
||||
assert.expect(1);
|
||||
const query = { depth: 0, version: 2 };
|
||||
const expectedQuery = { depth: '0', version: '2' };
|
||||
this.server.get(this.subkeysUrl, (schema, { queryParams }) => {
|
||||
assert.propEqual(queryParams, expectedQuery, `queryParams contain: ${JSON.stringify(queryParams)}`);
|
||||
return EXAMPLE_KV_SUBKEYS_RESPONSE;
|
||||
});
|
||||
|
||||
this.adapter.fetchSubkeys(this.backend, this.path, query);
|
||||
});
|
||||
});
|
||||
|
||||
module('patchSecret', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.adapter = this.store.adapterFor('kv/data');
|
||||
});
|
||||
|
||||
test('it should make request to patch', async function (assert) {
|
||||
assert.expect(2);
|
||||
const data = { foo: 'bar', baz: null };
|
||||
const expectedPayload = {
|
||||
data,
|
||||
options: {
|
||||
cas: 1,
|
||||
},
|
||||
};
|
||||
this.server.patch(this.endpoint('data'), (schema, req) => {
|
||||
const body = JSON.parse(req.requestBody);
|
||||
assert.true(true, `PATCH request made to ${this.endpoint('data')}`);
|
||||
assert.propEqual(body, expectedPayload, 'payload includes cas version');
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
|
||||
this.adapter.patchSecret(this.backend, this.path, data, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { kvMetadataPath } from 'vault/utils/kv-path';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
const UTC_DATE = '1994-11-05T00:00:00.000Z';
|
||||
|
||||
const EXAMPLE_KV_METADATA_GET_RESPONSE = {
|
||||
request_id: 'foobar',
|
||||
data: {
|
||||
cas_required: true,
|
||||
created_time: 'created-time',
|
||||
current_version: 2,
|
||||
custom_metadata: { application: 'staging' },
|
||||
delete_version_after: '0s',
|
||||
max_versions: 10,
|
||||
oldest_version: 0, // TODO: is this a bug? payload from real API
|
||||
updated_time: 'updated-time',
|
||||
versions: {
|
||||
1: {
|
||||
created_time: 'created-time',
|
||||
deletion_time: UTC_DATE,
|
||||
destroyed: false,
|
||||
},
|
||||
2: { created_time: 'created-time', deletion_time: '', destroyed: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module('Unit | Adapter | kv/metadata', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.backend = 'some/kv-back&end';
|
||||
this.secretMountPath.currentPath = this.backend;
|
||||
this.path = 'beep/bop my/secret';
|
||||
this.id = kvMetadataPath(this.backend, this.path);
|
||||
this.data = {
|
||||
options: {
|
||||
cas: 2,
|
||||
},
|
||||
data: {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
this.payload = {
|
||||
max_versions: 2,
|
||||
cas_required: false,
|
||||
delete_version_after: '0s',
|
||||
custom_metadata: {
|
||||
admin: 'bob',
|
||||
},
|
||||
};
|
||||
this.endpoint = kvMetadataPath(this.backend, this.path);
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on createRecord', async function (assert) {
|
||||
assert.expect(10);
|
||||
const recordData = {
|
||||
backend: this.backend,
|
||||
path: this.path,
|
||||
deleteVersionAfter: '45h',
|
||||
customMetadata: { application: 'staging' },
|
||||
oldestVersion: 4,
|
||||
currentVersion: 6,
|
||||
createdTime: 'created',
|
||||
updatedTime: 'updated',
|
||||
versions: {
|
||||
1: {
|
||||
created_time: 'created-time',
|
||||
deletion_time: UTC_DATE,
|
||||
destroyed: false,
|
||||
},
|
||||
2: {
|
||||
created_time: 'created-time',
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const expectedBody = {
|
||||
max_versions: 0,
|
||||
delete_version_after: '45h',
|
||||
cas_required: false,
|
||||
custom_metadata: { application: 'staging' },
|
||||
};
|
||||
this.server.post(this.endpoint, (schema, req) => {
|
||||
const body = JSON.parse(req.requestBody);
|
||||
assert.ok('POST request made to correct endpoint when creating new record');
|
||||
assert.propEqual(body, expectedBody, 'POST request has correct body');
|
||||
return new Response(204);
|
||||
});
|
||||
const record = this.store.createRecord('kv/metadata', recordData);
|
||||
await record.save();
|
||||
assert.strictEqual(record.id, this.id, 'record has correct id');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.maxVersions, 0, 'record has default maxVersions');
|
||||
assert.false(record.casRequired, 'record has correct casRequired');
|
||||
assert.strictEqual(record.deleteVersionAfter, '45h', 'record has correct deleteVersionAfter');
|
||||
assert.deepEqual(record.customMetadata, { application: 'staging' }, 'record has correct customMetadata');
|
||||
assert.deepEqual(
|
||||
record.versions,
|
||||
EXAMPLE_KV_METADATA_GET_RESPONSE.data.versions,
|
||||
'record has correct versions data'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on update record', async function (assert) {
|
||||
assert.expect(1);
|
||||
const data = this.server.create('kv-metadatum');
|
||||
data.id = kvMetadataPath('kv-engine', 'my-secret');
|
||||
this.store.pushPayload('kv/metadata', {
|
||||
modelName: 'kv/metadata',
|
||||
...data,
|
||||
});
|
||||
this.server.post(kvMetadataPath('kv-engine', 'my-secret'), () => {
|
||||
assert.ok(true, 'request made to correct endpoint on delete metadata.');
|
||||
});
|
||||
|
||||
const record = await this.store.peekRecord('kv/metadata', data.id);
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on queryRecord', async function (assert) {
|
||||
assert.expect(13);
|
||||
this.server.get(this.endpoint, () => {
|
||||
assert.ok(true, 'request is made to correct url on queryRecord.');
|
||||
return EXAMPLE_KV_METADATA_GET_RESPONSE;
|
||||
});
|
||||
|
||||
const record = await this.store.queryRecord('kv/metadata', { backend: this.backend, path: this.path });
|
||||
assert.strictEqual(record.id, this.id, 'record has correct id');
|
||||
assert.strictEqual(record.backend, this.backend, 'record has correct backend');
|
||||
assert.strictEqual(record.path, this.path, 'record has correct path');
|
||||
assert.strictEqual(record.maxVersions, 10, 'record has correct maxVersions');
|
||||
assert.true(record.casRequired, 'record has correct casRequired');
|
||||
assert.strictEqual(record.deleteVersionAfter, '0s', 'record has correct deleteVersionAfter');
|
||||
assert.deepEqual(record.customMetadata, { application: 'staging' }, 'record has correct customMetadata');
|
||||
assert.strictEqual(record.createdTime, 'created-time', 'record has correct createdTime');
|
||||
assert.strictEqual(record.currentVersion, 2, 'record has correct currentVersion');
|
||||
assert.strictEqual(record.oldestVersion, 0, 'record has correct oldestVersion');
|
||||
assert.strictEqual(record.updatedTime, 'updated-time', 'record has correct updatedTime');
|
||||
assert.deepEqual(
|
||||
record.versions,
|
||||
EXAMPLE_KV_METADATA_GET_RESPONSE.data.versions,
|
||||
'record has correct versions data'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on query', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get(kvMetadataPath(this.backend, 'directory/'), (schema, req) => {
|
||||
assert.ok(req.queryParams.list, 'list query param sent when listing secrets');
|
||||
return { data: { keys: [] } };
|
||||
});
|
||||
|
||||
this.store.query('kv/metadata', { backend: this.backend, pathToSecret: 'directory/' });
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint on delete metadata', async function (assert) {
|
||||
assert.expect(3);
|
||||
const data = this.server.create('kv-metadatum');
|
||||
data.id = kvMetadataPath('kv-engine', 'my-secret');
|
||||
this.store.pushPayload('kv/metadata', {
|
||||
modelName: 'kv/metadata',
|
||||
...data,
|
||||
});
|
||||
this.server.delete(kvMetadataPath('kv-engine', 'my-secret'), () => {
|
||||
assert.ok(true, 'request made to correct endpoint on delete metadata.');
|
||||
});
|
||||
|
||||
let record = await this.store.peekRecord('kv/metadata', data.id);
|
||||
|
||||
await record.destroyRecord();
|
||||
assert.true(record.isDeleted, 'record is deleted');
|
||||
record = await this.store.peekRecord('kv/metadata', this.id);
|
||||
assert.strictEqual(record, null, 'record is no longer in store');
|
||||
});
|
||||
});
|
||||
|
|
@ -27,65 +27,116 @@ module('Unit | Decorators | ModelFormFields', function (hooks) {
|
|||
assert.ok(this.spy.calledWith(message), 'Error is printed to console');
|
||||
});
|
||||
|
||||
test('it return allFields when arguments not provided', function (assert) {
|
||||
test('it returns allFields when arguments not provided', function (assert) {
|
||||
assert.expect(1);
|
||||
// test by instantiating a record that uses this decorator
|
||||
const record = this.store.createRecord('kv/data');
|
||||
const record = this.store.createRecord('totp-key');
|
||||
assert.deepEqual(
|
||||
record.allFields,
|
||||
[
|
||||
{
|
||||
name: 'backend',
|
||||
options: {},
|
||||
options: { readOnly: true },
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
name: 'name',
|
||||
options: {
|
||||
label: 'Path for this secret',
|
||||
subText: 'Names with forward slashes define hierarchical path structures.',
|
||||
subText: 'Specifies the name for this key.',
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'secretData',
|
||||
options: {},
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
name: 'createdTime',
|
||||
options: {},
|
||||
name: 'accountName',
|
||||
options: {
|
||||
subText: 'The name of the account associated with the key. Required for keys generated by Vault.',
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'customMetadata',
|
||||
options: {},
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
name: 'deletionTime',
|
||||
options: {},
|
||||
name: 'algorithm',
|
||||
options: { possibleValues: ['SHA1', 'SHA256', 'SHA512'], defaultValue: 'SHA1' },
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'destroyed',
|
||||
options: {},
|
||||
name: 'digits',
|
||||
options: { possibleValues: [6, 8], defaultValue: 6 },
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'issuer',
|
||||
options: {
|
||||
subText: `The name of the key's issuing organization. Required for keys generated by Vault.`,
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'period',
|
||||
options: {
|
||||
editType: 'ttl',
|
||||
helperTextEnabled: 'How long each generated TOTP is valid.',
|
||||
defaultValue: 30,
|
||||
},
|
||||
type: undefined,
|
||||
},
|
||||
{
|
||||
name: 'generate',
|
||||
options: {
|
||||
label: 'Key Provider',
|
||||
defaultValue: true,
|
||||
editType: 'radio',
|
||||
possibleValues: ['Vault', 'Other service'],
|
||||
fieldValue: 'generateString',
|
||||
subText: 'Specifies if the key should be generated by Vault or passed from another service.',
|
||||
},
|
||||
type: undefined,
|
||||
},
|
||||
{
|
||||
name: 'keySize',
|
||||
options: { defaultValue: 20 },
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'skew',
|
||||
options: { possibleValues: [0, 1], defaultValue: 1 },
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'exported',
|
||||
options: {
|
||||
editType: 'toggleButton',
|
||||
defaultValue: true,
|
||||
helperTextDisabled: 'Vault will not return QR code and url upon key creation.',
|
||||
helperTextEnabled: 'QR code and URL will be returned upon generating a key.',
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'version',
|
||||
options: {},
|
||||
name: 'qrSize',
|
||||
options: { label: 'QR size', defaultValue: 200 },
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'failReadErrorCode',
|
||||
options: {},
|
||||
type: 'number',
|
||||
name: 'url',
|
||||
options: {
|
||||
label: 'URL',
|
||||
helpText:
|
||||
'If a URL is provided the other fields can be left empty. E.g. otpauth://totp/Vault:test@test.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Vault',
|
||||
subText: 'The TOTP key url string that can be used to configure a key.',
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'casVersion',
|
||||
options: {},
|
||||
type: 'number',
|
||||
name: 'key',
|
||||
options: {
|
||||
subText: 'The root key used to generate a TOTP code.',
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'barcode',
|
||||
options: { readOnly: true },
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
'allFields set on Model class'
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | kv/data', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it should always pass the cas option when creating/updating a secret', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('kv/data', {
|
||||
path: 'my-secret-path',
|
||||
backend: 'kv-test',
|
||||
version: 2,
|
||||
casVersion: 3,
|
||||
secretData: { foo: 'bar' },
|
||||
});
|
||||
const expectedResult = {
|
||||
data: { foo: 'bar' },
|
||||
options: {
|
||||
cas: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const serializedRecord = record.serialize();
|
||||
assert.deepEqual(serializedRecord, expectedResult, 'cas option was correctly added to the payload.');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | kv/metadata', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it should properly normalize a list response', function (assert) {
|
||||
const serializer = this.owner.lookup('serializer:kv/metadata');
|
||||
const serverData = {
|
||||
request_id: 'foo',
|
||||
backend: 'my-backend',
|
||||
path: '',
|
||||
data: {
|
||||
keys: ['first', 'second', 'third/'],
|
||||
},
|
||||
};
|
||||
const expectedData = [
|
||||
{
|
||||
id: 'my-backend/metadata/first',
|
||||
path: 'first',
|
||||
backend: 'my-backend',
|
||||
full_secret_path: 'first',
|
||||
},
|
||||
{
|
||||
id: 'my-backend/metadata/second',
|
||||
path: 'second',
|
||||
backend: 'my-backend',
|
||||
full_secret_path: 'second',
|
||||
},
|
||||
{
|
||||
id: 'my-backend/metadata/third/',
|
||||
path: 'third/',
|
||||
backend: 'my-backend',
|
||||
full_secret_path: 'third/',
|
||||
},
|
||||
];
|
||||
|
||||
const serializedRecord = serializer.normalizeItems(serverData);
|
||||
assert.deepEqual(serializedRecord, expectedData, 'transformed keys into proper IDs');
|
||||
});
|
||||
|
||||
test('it should properly normalize a nested secret list response', function (assert) {
|
||||
const serializer = this.owner.lookup('serializer:kv/metadata');
|
||||
const serverData = {
|
||||
request_id: 'foo',
|
||||
backend: 'my-backend',
|
||||
path: 'beep/',
|
||||
data: {
|
||||
keys: ['boop/'],
|
||||
},
|
||||
};
|
||||
const expectedData = [
|
||||
{
|
||||
id: 'my-backend/metadata/beep/boop/',
|
||||
path: 'boop/',
|
||||
backend: 'my-backend',
|
||||
full_secret_path: 'beep/boop/',
|
||||
},
|
||||
];
|
||||
const serializedRecord = serializer.normalizeItems(serverData);
|
||||
assert.deepEqual(serializedRecord, expectedData, 'transformed keys into proper IDs');
|
||||
});
|
||||
|
||||
test('it throws an assertion if backend not on payload', function (assert) {
|
||||
const serializer = this.owner.lookup('serializer:kv/metadata');
|
||||
const serverData = {
|
||||
request_id: 'foo',
|
||||
data: {
|
||||
keys: ['first', 'second'],
|
||||
},
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = serializer.normalizeItems(serverData);
|
||||
} catch (e) {
|
||||
result = e.message;
|
||||
}
|
||||
assert.strictEqual(
|
||||
result,
|
||||
'Assertion Failed: payload.backend must be provided on kv/metadata list response'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -97,7 +97,7 @@ module('Unit | Service | path-help', function (hooks) {
|
|||
assert.notOk(true, 'this method should not be called');
|
||||
return reject();
|
||||
});
|
||||
const modelType = 'kv/data';
|
||||
const modelType = 'totp-key';
|
||||
await this.pathHelp.getNewModel(modelType, 'my-kv').then(() => {
|
||||
assert.true(true, 'getNewModel resolves');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import Adapter from 'ember-data/adapter';
|
|||
import ModelRegistry from 'ember-data/types/registries/model';
|
||||
|
||||
import ClientsActivityAdapter from 'vault/vault/adapters/clients/activity';
|
||||
import KvDataAdapter from 'vault/adapters/kv/data';
|
||||
import KvMetadataAdapter from 'vault/adapters/kv/metadata';
|
||||
import LdapLibraryAdapter from 'vault/adapters/ldap/library';
|
||||
import LdapRoleAdapter from 'vault/adapters/ldap/role';
|
||||
import PkiIssuerAdapter from 'vault/adapters/pki/issuer';
|
||||
|
|
@ -26,8 +24,6 @@ export default interface AdapterRegistry {
|
|||
'ldap/role': LdapRoleAdapter;
|
||||
'pki/issuer': PkiIssuerAdapter;
|
||||
'pki/tidy': PkiTidyAdapter;
|
||||
'kv/data': KvDataAdapterAdapter;
|
||||
'kv/metadata': KvMetadataAdapter;
|
||||
'sync/destination': SyncDestinationAdapter;
|
||||
'sync/association': SyncAssociationAdapter;
|
||||
application: Application;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
*/
|
||||
|
||||
import Model from '@ember-data/model';
|
||||
import KvSecretDataModel from 'vault/models/kv/data';
|
||||
import KvSecretMetadataModel from 'vault/models/kv/metadata';
|
||||
import PkiActionModel from 'vault/models/pki/action';
|
||||
import PkiCertificateGenerateModel from 'vault/models/pki/certificate/generate';
|
||||
import PkiConfigAcmeModel from 'vault/models/pki/config/acme';
|
||||
|
|
@ -24,8 +22,6 @@ declare module 'ember-data/types/registries/model' {
|
|||
'pki/config/cluster': PkiConfigClusterModel;
|
||||
'pki/config/crl': PkiConfigCrlModel;
|
||||
'pki/config/urls': PkiConfigUrlModel;
|
||||
'kv/data': KvSecretDataModel;
|
||||
'kv/metadata': KvSecretMetadataModel;
|
||||
'clients/activity': ClientsActivityModel;
|
||||
'clients/config': ClientsConfigModel;
|
||||
'clients/version-history': ClientsVersionHistoryModel;
|
||||
|
|
|
|||
Loading…
Reference in a new issue