mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 11:38:02 +00:00
UI - write without read for kv (#6570)
* wait for all hash promises to be settled * skeleton tests with policies for write without read * adjust what gets returned from the model hook * refactor secret-edit model hook to use async/await * return a stub version if we can't read secret data * return a stub model for v1 kv * tweak tests to make re-runs friendlier * allow write without CAS if both v2 models cannot be read * show warnings on edit pages for different write without read scenarios * add no read empty states on secret show pages * review feedback * make message language consistent * use version models from metadata if we can read it * refresh route on delete / undelete / destroy * hide controls in the toolbar when you can't read the secret data * show deleted / destroyed messaging over cannot read messaging on the show page * fix test with model stub * refactor large model hook into several functions * comment clarifications
This commit is contained in:
@@ -74,7 +74,7 @@ export default ApplicationAdapter.extend({
|
||||
return this.ajax(this._url(backend, path, deleteType), 'POST', { data: { versions: [version] } }).then(
|
||||
() => {
|
||||
let model = store.peekRecord('secret-v2-version', id);
|
||||
return model && model.reload();
|
||||
return model && model.rollbackAttributes() && model.reload();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -156,6 +156,22 @@ export default Component.extend(FocusOnInsertMixin, WithNavToNearestAncestor, {
|
||||
return this.secretDataIsAdvanced || this.preferAdvancedEdit;
|
||||
}),
|
||||
|
||||
isWriteWithoutRead: computed(
|
||||
'model.{failedServerRead,selectedVersion.failedServerRead}',
|
||||
'isV2',
|
||||
function() {
|
||||
// if the version couldn't be read from the server
|
||||
if (this.isV2 && this.model.selectedVersion.failedServerRead) {
|
||||
return true;
|
||||
}
|
||||
// if the model couldn't be read from the server
|
||||
if (!this.isV2 && this.model.failedServerRead) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
),
|
||||
|
||||
transitionToRoute() {
|
||||
return this.router.transitionTo(...arguments);
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ export default Component.extend({
|
||||
store: service(),
|
||||
version: null,
|
||||
useDefaultTrigger: false,
|
||||
onRefresh() {},
|
||||
|
||||
deleteVersionPath: maybeQueryRecord(
|
||||
'capabilities',
|
||||
@@ -52,7 +53,8 @@ export default Component.extend({
|
||||
deleteVersion(deleteType = 'destroy') {
|
||||
return this.store
|
||||
.adapterFor('secret-v2-version')
|
||||
.v2DeleteOperation(this.store, this.version.id, deleteType);
|
||||
.v2DeleteOperation(this.store, this.version.id, deleteType)
|
||||
.then(this.onRefresh);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ export default DS.Model.extend(KeyMixin, {
|
||||
|
||||
isAdvancedFormat: computed('secretData', function() {
|
||||
const data = this.get('secretData');
|
||||
return Object.keys(data).some(key => typeof data[key] !== 'string');
|
||||
return data && Object.keys(data).some(key => typeof data[key] !== 'string');
|
||||
}),
|
||||
|
||||
helpText: attr('string'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { set } from '@ember/object';
|
||||
import { hash, resolve } from 'rsvp';
|
||||
import { resolve } from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DS from 'ember-data';
|
||||
import Route from '@ember/routing/route';
|
||||
@@ -21,12 +21,11 @@ export default Route.extend(UnloadModelRoute, {
|
||||
capabilities(secret) {
|
||||
const backend = this.enginePathParam();
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
let backendType = backendModel.get('engineType');
|
||||
if (backendType === 'kv' || backendType === 'cubbyhole' || backendType === 'generic') {
|
||||
return resolve({});
|
||||
}
|
||||
let backendType = backendModel.engineType;
|
||||
let path;
|
||||
if (backendType === 'transit') {
|
||||
if (backendModel.isV2KV) {
|
||||
path = `${backend}/data/${secret}`;
|
||||
} else if (backendType === 'transit') {
|
||||
path = backend + '/keys/' + secret;
|
||||
} else if (backendType === 'ssh' || backendType === 'aws') {
|
||||
path = backend + '/roles/' + secret;
|
||||
@@ -43,9 +42,6 @@ export default Route.extend(UnloadModelRoute, {
|
||||
templateName: 'vault/cluster/secrets/backend/secretEditLayout',
|
||||
|
||||
beforeModel() {
|
||||
// currently there is no recursive delete for folders in vault, so there's no need to 'edit folders'
|
||||
// perhaps in the future we could recurse _for_ users, but for now, just kick them
|
||||
// back to the list
|
||||
let secret = this.secretParam();
|
||||
return this.buildModel(secret).then(() => {
|
||||
const parentKey = utils.parentKeyForKey(secret);
|
||||
@@ -86,10 +82,106 @@ export default Route.extend(UnloadModelRoute, {
|
||||
return types[type];
|
||||
},
|
||||
|
||||
model(params) {
|
||||
let secret = this.secretParam();
|
||||
getTargetVersion(currentVersion, paramsVersion) {
|
||||
if (currentVersion) {
|
||||
// we have the secret metadata, so we can read the currentVersion but give priority to any
|
||||
// version passed in via the url
|
||||
return parseInt(paramsVersion || currentVersion, 10);
|
||||
} else {
|
||||
// we've got a stub model because don't have read access on the metadata endpoint
|
||||
return paramsVersion ? parseInt(paramsVersion, 10) : null;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchV2Models(capabilities, secretModel, params) {
|
||||
let backend = this.enginePathParam();
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
|
||||
let targetVersion = this.getTargetVersion(secretModel.currentVersion, params.version);
|
||||
|
||||
// if we have the metadata, a list of versions are part of the payload
|
||||
let version = secretModel.versions && secretModel.versions.findBy('version', targetVersion);
|
||||
// if it didn't fail the server read, and the version is not attached to the metadata,
|
||||
// this should 404
|
||||
if (!version && secretModel.failedServerRead !== true) {
|
||||
let error = new DS.AdapterError();
|
||||
set(error, 'httpStatus', 404);
|
||||
throw error;
|
||||
}
|
||||
// manually set the related model
|
||||
secretModel.set('engine', backendModel);
|
||||
|
||||
secretModel.set(
|
||||
'selectedVersion',
|
||||
await this.fetchV2VersionModel(capabilities, secretModel, version, targetVersion)
|
||||
);
|
||||
return secretModel;
|
||||
},
|
||||
|
||||
async fetchV2VersionModel(capabilities, secretModel, version, targetVersion) {
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
|
||||
// v2 versions have a composite ID, we generated one here if we need to manually set it
|
||||
// after a failed fetch later;
|
||||
let versionId = targetVersion ? [backend, secret, targetVersion] : [backend, secret];
|
||||
|
||||
let versionModel;
|
||||
try {
|
||||
if (secretModel.failedServerRead) {
|
||||
// we couldn't read metadata, so we want to directly fetch the version
|
||||
versionModel = await this.store.findRecord('secret-v2-version', JSON.stringify(versionId), {
|
||||
reload: true,
|
||||
});
|
||||
} else {
|
||||
// we may have previously errored, so roll it back here
|
||||
version.rollbackAttributes();
|
||||
// if metadata read was successful, the version we have is only a partial model
|
||||
// trigger reload to fetch the whole version model
|
||||
versionModel = await version.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
// cannot read the version data, but can write according to capabilities-self endpoint
|
||||
if (error.httpStatus === 403 && capabilities.get('canUpdate')) {
|
||||
// versionModel is then a partial model from the metadata (if we have read there), or
|
||||
// we need to create one on the client
|
||||
versionModel = version || this.store.createRecord('secret-v2-version');
|
||||
versionModel.setProperties({
|
||||
failedServerRead: true,
|
||||
});
|
||||
// if it was created on the client we need to trigger an event via ember-data
|
||||
// so that it won't try to create the record on save
|
||||
if (versionModel.isNew) {
|
||||
versionModel.set('id', JSON.stringify(versionId));
|
||||
//TODO make this a util to better show what's happening
|
||||
// this is because we want the ember-data model save to call update instead of create
|
||||
// in the adapter so we have to force the frontend model to a "saved" state
|
||||
versionModel.send('pushedData');
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return versionModel;
|
||||
},
|
||||
|
||||
handleSecretModelError(capabilities, secret, modelType, error) {
|
||||
// can't read the path and don't have update capability, so re-throw
|
||||
if (!capabilities.get('canUpdate') && modelType === 'secret') {
|
||||
throw error;
|
||||
}
|
||||
// don't have access to the metadata for v2 or the secret for v1,
|
||||
// so we make a stub model and mark it as `failedServerRead`
|
||||
let secretModel = this.store.createRecord(modelType);
|
||||
secretModel.setProperties({
|
||||
id: secret,
|
||||
failedServerRead: true,
|
||||
});
|
||||
return secretModel;
|
||||
},
|
||||
|
||||
async model(params) {
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
let modelType = this.modelType(backend, secret);
|
||||
|
||||
if (!secret) {
|
||||
@@ -98,53 +190,31 @@ export default Route.extend(UnloadModelRoute, {
|
||||
if (modelType === 'pki-certificate') {
|
||||
secret = secret.replace('cert/', '');
|
||||
}
|
||||
return hash({
|
||||
secret: this.store
|
||||
.queryRecord(modelType, { id: secret, backend })
|
||||
.then(secretModel => {
|
||||
if (modelType === 'secret-v2') {
|
||||
let targetVersion = parseInt(params.version || secretModel.currentVersion, 10);
|
||||
let version = secretModel.versions.findBy('version', targetVersion);
|
||||
// 404 if there's no version
|
||||
if (!version) {
|
||||
let error = new DS.AdapterError();
|
||||
set(error, 'httpStatus', 404);
|
||||
throw error;
|
||||
}
|
||||
secretModel.set('engine', backendModel);
|
||||
let secretModel;
|
||||
|
||||
return version.reload().then(() => {
|
||||
secretModel.set('selectedVersion', version);
|
||||
return secretModel;
|
||||
});
|
||||
}
|
||||
return secretModel;
|
||||
})
|
||||
.catch(err => {
|
||||
//don't have access to the metadata, so we'll make
|
||||
//a stub metadata model and try to load the version
|
||||
if (modelType === 'secret-v2' && err.httpStatus === 403) {
|
||||
let secretModel = this.store.createRecord('secret-v2');
|
||||
secretModel.setProperties({
|
||||
engine: backendModel,
|
||||
id: secret,
|
||||
// so we know it's a stub model and won't be saving it
|
||||
// because we don't have access to that endpoint
|
||||
isStub: true,
|
||||
});
|
||||
let targetVersion = params.version ? parseInt(params.version, 10) : null;
|
||||
let versionId = targetVersion ? [backend, secret, targetVersion] : [backend, secret];
|
||||
return this.store
|
||||
.findRecord('secret-v2-version', JSON.stringify(versionId), { reload: true })
|
||||
.then(versionModel => {
|
||||
secretModel.set('selectedVersion', versionModel);
|
||||
return secretModel;
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}),
|
||||
capabilities: this.capabilities(secret),
|
||||
});
|
||||
let capabilities = this.capabilities(secret);
|
||||
try {
|
||||
secretModel = await this.store.queryRecord(modelType, { id: secret, backend });
|
||||
} catch (err) {
|
||||
// we've failed the read request, but if it's a kv-type backend, we want to
|
||||
// do additional checks of the capabilities
|
||||
if (err.httpStatus === 403 && (modelType === 'secret-v2' || modelType === 'secret')) {
|
||||
await capabilities;
|
||||
secretModel = this.handleSecretModelError(capabilities, secret, modelType, err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await capabilities;
|
||||
if (modelType === 'secret-v2') {
|
||||
// after the the base model fetch, kv-v2 has a second associated
|
||||
// version model that contains the secret data
|
||||
secretModel = await this.fetchV2Models(capabilities, secretModel, params);
|
||||
}
|
||||
return {
|
||||
secret: secretModel,
|
||||
capabilities,
|
||||
};
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
|
||||
@@ -18,7 +18,13 @@ export default ApplicationSerializer.extend({
|
||||
},
|
||||
serialize(snapshot) {
|
||||
let secret = snapshot.belongsTo('secret');
|
||||
let version = secret.record.isStub ? snapshot.attr('version') : secret.attr('currentVersion');
|
||||
// if both models failed to read from the server, we need to write without CAS
|
||||
if (secret.record.failedServerRead && snapshot.record.failedServerRead) {
|
||||
return {
|
||||
data: snapshot.attr('secretData'),
|
||||
};
|
||||
}
|
||||
let version = secret.record.failedServerRead ? snapshot.attr('version') : secret.attr('currentVersion');
|
||||
version = version || 0;
|
||||
return {
|
||||
data: snapshot.attr('secretData'),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="empty-state">
|
||||
<div class="empty-state" ...attributes>
|
||||
<div class="empty-state-content">
|
||||
<h3 class="empty-state-title">
|
||||
{{title}}
|
||||
</h3>
|
||||
{{#if message}}
|
||||
<p class="empty-state-message">
|
||||
<p class="empty-state-message" data-test-empty-state-message>
|
||||
{{message}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{#if (and (or @model.isNew @canEditV2Secret) @isV2 (not @model.isStub))}}
|
||||
{{#if (and (or @model.isNew @canEditV2Secret) @isV2 (not @model.failedServerRead))}}
|
||||
<div data-test-metadata-fields class="form-section box is-shadowless is-fullwidth">
|
||||
<label class="title is-5">
|
||||
Secret metadata
|
||||
@@ -9,6 +9,31 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if @showWriteWithoutReadWarning}}
|
||||
{{#if (and @isV2 @model.failedServerRead)}}
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@message="Your policies prevent you from reading metadata for this secret and the current version's data. Creating a new version of the secret with this form will not be able to use the check-and-set mechanism. If this is required on the secret, then you will need access to read the secret's metadata."
|
||||
@class="is-marginless"
|
||||
data-test-v2-no-cas-warning
|
||||
/>
|
||||
{{else if @isV2}}
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@message="Your policies prevent you from reading the current secret version. Saving this form will create a new version of the secret and will utilize the available check-and-set mechanism."
|
||||
@class="is-marginless"
|
||||
data-test-v2-write-without-read
|
||||
/>
|
||||
{{else}}
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@message="Your policies prevent you from reading the current secret data. Saving using this form will overwrite the existing values."
|
||||
@class="is-marginless"
|
||||
data-test-v1-write-without-read
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @showAdvancedMode}}
|
||||
<div class="form-section">
|
||||
<label class="title is-5">
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</p.levelRight>
|
||||
</PageHeader>
|
||||
<div class="secret-control-bar">
|
||||
{{#unless (and (eq mode 'show') isWriteWithoutRead)}}
|
||||
<div class="control">
|
||||
<input
|
||||
data-test-secret-json-toggle=true
|
||||
@@ -52,9 +53,10 @@
|
||||
/>
|
||||
<label for="json" class="has-text-grey">JSON</label>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
||||
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
|
||||
{{#unless (and isV2 (or modelForData.destroyed modelForData.deleted))}}
|
||||
{{#unless (and isV2 (or isWriteWithoutRead modelForData.destroyed modelForData.deleted))}}
|
||||
<div class="control">
|
||||
<BasicDropdown
|
||||
@class="popup-menu"
|
||||
@@ -134,9 +136,12 @@
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
{{#if (and (eq @mode "show") this.isV2)}}
|
||||
{{#if (and (eq @mode "show") this.isV2 (not @model.failedServerRead))}}
|
||||
<div class="control">
|
||||
<SecretVersionMenu @version={{this.modelForData}} />
|
||||
<SecretVersionMenu
|
||||
@version={{this.modelForData}}
|
||||
@onRefresh={{action 'refresh'}}
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<BasicDropdown
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-paddingless">
|
||||
<MessageError @model={{model}} @errorMessage={{error}} />
|
||||
<NamespaceReminder @mode="edit" @noun="secret" />
|
||||
{{#if (and (not model.isStub) (not-eq model.selectedVersion.version model.currentVersion))}}
|
||||
{{#if (and (not model.failedServerRead) (not model.selectedVersion.failedServerRead) (not-eq model.selectedVersion.version model.currentVersion))}}
|
||||
<div class="form-section">
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@@ -17,6 +17,7 @@
|
||||
@secretData={{secretData}}
|
||||
@isV2={{isV2}}
|
||||
@canEditV2Secret={{canEditV2Secret}}
|
||||
@showWriteWithoutReadWarning={{isWriteWithoutRead}}
|
||||
@model={{model}}
|
||||
@editActions={{hash
|
||||
codemirrorUpdated=(action "codemirrorUpdated")
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
Learn more
|
||||
</DocLink>
|
||||
</EmptyState>
|
||||
{{else if isWriteWithoutRead}}
|
||||
<EmptyState
|
||||
data-test-write-without-read-empty-message
|
||||
@title="You do not have permission to read this secret."
|
||||
@message={{if isV2
|
||||
"Your policies permit you to write a new version of this secret, but do not allow you to read its current contents."
|
||||
"Your policies permit you to overwrite this secret, but do not allow you to read it."
|
||||
}}
|
||||
>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
{{#if showAdvancedMode}}
|
||||
{{json-editor
|
||||
|
||||
@@ -324,4 +324,108 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
||||
assert.equal(listPage.secrets.length, 3, 'renders three secrets');
|
||||
assert.equal(listPage.filterInputValue, 'filter/', 'pageFilter has been reset');
|
||||
});
|
||||
|
||||
let setupNoRead = async function(backend, canReadMeta = false) {
|
||||
const V2_WRITE_ONLY_POLICY = `'
|
||||
path "${backend}/+/+" {
|
||||
capabilities = ["create", "update", "list"]
|
||||
}
|
||||
path "${backend}/+" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
'`;
|
||||
|
||||
const V2_WRITE_WITH_META_READ_POLICY = `'
|
||||
path "${backend}/+/+" {
|
||||
capabilities = ["create", "update", "list"]
|
||||
}
|
||||
path "${backend}/metadata/+" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "${backend}/+" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
'`;
|
||||
const V1_WRITE_ONLY_POLICY = `'
|
||||
path "${backend}/+" {
|
||||
capabilities = ["create", "update", "list"]
|
||||
}
|
||||
'`;
|
||||
|
||||
let policy;
|
||||
if (backend === 'kv-v2' && canReadMeta) {
|
||||
policy = V2_WRITE_WITH_META_READ_POLICY;
|
||||
} else if (backend === 'kv-v2') {
|
||||
policy = V2_WRITE_ONLY_POLICY;
|
||||
} else if (backend === 'kv-v1') {
|
||||
policy = V1_WRITE_ONLY_POLICY;
|
||||
}
|
||||
await consoleComponent.runCommands([
|
||||
// disable any kv previously enabled kv
|
||||
`delete sys/mounts/${backend}`,
|
||||
`write sys/mounts/${backend} type=kv options=version=${backend === 'kv-v2' ? 2 : 1}`,
|
||||
`write sys/policies/acl/${backend} policy=${policy}`,
|
||||
`write -field=client_token auth/token/create policies=${backend}`,
|
||||
]);
|
||||
|
||||
return consoleComponent.lastLogOutput;
|
||||
};
|
||||
test('write without read: version 2', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
let userToken = await setupNoRead(backend);
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await showPage.visit({ backend, id: 'secret' });
|
||||
assert.ok(showPage.noReadIsPresent, 'shows no read empty state');
|
||||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
|
||||
await editPage.visitEdit({ backend, id: 'secret' });
|
||||
assert.notOk(editPage.hasMetadataFields, 'hides the metadata form');
|
||||
assert.ok(editPage.showsNoCASWarning, 'shows no CAS write warning');
|
||||
|
||||
await editPage.editSecret('bar', 'baz');
|
||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
|
||||
await logout.visit();
|
||||
});
|
||||
|
||||
test('write without read: version 2 with metadata read', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
let userToken = await setupNoRead(backend, true);
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await showPage.visit({ backend, id: 'secret' });
|
||||
assert.ok(showPage.noReadIsPresent, 'shows no read empty state');
|
||||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
|
||||
await editPage.visitEdit({ backend, id: 'secret' });
|
||||
assert.notOk(editPage.hasMetadataFields, 'hides the metadata form');
|
||||
assert.ok(editPage.showsV2WriteWarning, 'shows v2 warning');
|
||||
|
||||
await editPage.editSecret('bar', 'baz');
|
||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
|
||||
await logout.visit();
|
||||
});
|
||||
|
||||
test('write without read: version 1', async function(assert) {
|
||||
let backend = 'kv-v1';
|
||||
let userToken = await setupNoRead(backend);
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await showPage.visit({ backend, id: 'secret' });
|
||||
assert.ok(showPage.noReadIsPresent, 'shows no read empty state');
|
||||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
|
||||
await editPage.visitEdit({ backend, id: 'secret' });
|
||||
assert.ok(editPage.showsV1WriteWarning, 'shows v1 warning');
|
||||
|
||||
await editPage.editSecret('bar', 'baz');
|
||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
|
||||
await logout.visit();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ export default create({
|
||||
visitEditRoot: visitable('/vault/secrets/:backend/edit'),
|
||||
toggleJSON: clickable('[data-test-secret-json-toggle]'),
|
||||
hasMetadataFields: isPresent('[data-test-metadata-fields]'),
|
||||
showsNoCASWarning: isPresent('[data-test-v2-no-cas-warning]'),
|
||||
showsV2WriteWarning: isPresent('[data-test-v2-write-without-read]'),
|
||||
showsV1WriteWarning: isPresent('[data-test-v1-write-without-read]'),
|
||||
editor: {
|
||||
fillIn: codeFillable('[data-test-component="json-editor"]'),
|
||||
},
|
||||
|
||||
@@ -14,6 +14,8 @@ export default create({
|
||||
toggleIsPresent: isPresent('[data-test-secret-json-toggle]'),
|
||||
edit: clickable('[data-test-secret-edit]'),
|
||||
editIsPresent: isPresent('[data-test-secret-edit]'),
|
||||
noReadIsPresent: isPresent('[data-test-write-without-read-empty-message]'),
|
||||
noReadMessage: text('data-test-empty-state-message'),
|
||||
editor: {
|
||||
content: code('[data-test-component="json-editor"]'),
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ module('Unit | Adapter | secret-v2-version', function(hooks) {
|
||||
let fakeStore = {
|
||||
peekRecord() {
|
||||
return {
|
||||
rollbackAttributes() {},
|
||||
reload() {},
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user