mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +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(
|
return this.ajax(this._url(backend, path, deleteType), 'POST', { data: { versions: [version] } }).then(
|
||||||
() => {
|
() => {
|
||||||
let model = store.peekRecord('secret-v2-version', id);
|
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;
|
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() {
|
transitionToRoute() {
|
||||||
return this.router.transitionTo(...arguments);
|
return this.router.transitionTo(...arguments);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default Component.extend({
|
|||||||
store: service(),
|
store: service(),
|
||||||
version: null,
|
version: null,
|
||||||
useDefaultTrigger: false,
|
useDefaultTrigger: false,
|
||||||
|
onRefresh() {},
|
||||||
|
|
||||||
deleteVersionPath: maybeQueryRecord(
|
deleteVersionPath: maybeQueryRecord(
|
||||||
'capabilities',
|
'capabilities',
|
||||||
@@ -52,7 +53,8 @@ export default Component.extend({
|
|||||||
deleteVersion(deleteType = 'destroy') {
|
deleteVersion(deleteType = 'destroy') {
|
||||||
return this.store
|
return this.store
|
||||||
.adapterFor('secret-v2-version')
|
.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() {
|
isAdvancedFormat: computed('secretData', function() {
|
||||||
const data = this.get('secretData');
|
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'),
|
helpText: attr('string'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { set } from '@ember/object';
|
import { set } from '@ember/object';
|
||||||
import { hash, resolve } from 'rsvp';
|
import { resolve } from 'rsvp';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import DS from 'ember-data';
|
import DS from 'ember-data';
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
@@ -21,12 +21,11 @@ export default Route.extend(UnloadModelRoute, {
|
|||||||
capabilities(secret) {
|
capabilities(secret) {
|
||||||
const backend = this.enginePathParam();
|
const backend = this.enginePathParam();
|
||||||
let backendModel = this.modelFor('vault.cluster.secrets.backend');
|
let backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||||
let backendType = backendModel.get('engineType');
|
let backendType = backendModel.engineType;
|
||||||
if (backendType === 'kv' || backendType === 'cubbyhole' || backendType === 'generic') {
|
|
||||||
return resolve({});
|
|
||||||
}
|
|
||||||
let path;
|
let path;
|
||||||
if (backendType === 'transit') {
|
if (backendModel.isV2KV) {
|
||||||
|
path = `${backend}/data/${secret}`;
|
||||||
|
} else if (backendType === 'transit') {
|
||||||
path = backend + '/keys/' + secret;
|
path = backend + '/keys/' + secret;
|
||||||
} else if (backendType === 'ssh' || backendType === 'aws') {
|
} else if (backendType === 'ssh' || backendType === 'aws') {
|
||||||
path = backend + '/roles/' + secret;
|
path = backend + '/roles/' + secret;
|
||||||
@@ -43,9 +42,6 @@ export default Route.extend(UnloadModelRoute, {
|
|||||||
templateName: 'vault/cluster/secrets/backend/secretEditLayout',
|
templateName: 'vault/cluster/secrets/backend/secretEditLayout',
|
||||||
|
|
||||||
beforeModel() {
|
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();
|
let secret = this.secretParam();
|
||||||
return this.buildModel(secret).then(() => {
|
return this.buildModel(secret).then(() => {
|
||||||
const parentKey = utils.parentKeyForKey(secret);
|
const parentKey = utils.parentKeyForKey(secret);
|
||||||
@@ -86,10 +82,106 @@ export default Route.extend(UnloadModelRoute, {
|
|||||||
return types[type];
|
return types[type];
|
||||||
},
|
},
|
||||||
|
|
||||||
model(params) {
|
getTargetVersion(currentVersion, paramsVersion) {
|
||||||
let secret = this.secretParam();
|
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 backend = this.enginePathParam();
|
||||||
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
|
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);
|
let modelType = this.modelType(backend, secret);
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
@@ -98,53 +190,31 @@ export default Route.extend(UnloadModelRoute, {
|
|||||||
if (modelType === 'pki-certificate') {
|
if (modelType === 'pki-certificate') {
|
||||||
secret = secret.replace('cert/', '');
|
secret = secret.replace('cert/', '');
|
||||||
}
|
}
|
||||||
return hash({
|
let secretModel;
|
||||||
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);
|
|
||||||
|
|
||||||
return version.reload().then(() => {
|
let capabilities = this.capabilities(secret);
|
||||||
secretModel.set('selectedVersion', version);
|
try {
|
||||||
return secretModel;
|
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
|
||||||
return secretModel;
|
// do additional checks of the capabilities
|
||||||
})
|
if (err.httpStatus === 403 && (modelType === 'secret-v2' || modelType === 'secret')) {
|
||||||
.catch(err => {
|
await capabilities;
|
||||||
//don't have access to the metadata, so we'll make
|
secretModel = this.handleSecretModelError(capabilities, secret, modelType, err);
|
||||||
//a stub metadata model and try to load the version
|
} else {
|
||||||
if (modelType === 'secret-v2' && err.httpStatus === 403) {
|
throw err;
|
||||||
let secretModel = this.store.createRecord('secret-v2');
|
}
|
||||||
secretModel.setProperties({
|
}
|
||||||
engine: backendModel,
|
await capabilities;
|
||||||
id: secret,
|
if (modelType === 'secret-v2') {
|
||||||
// so we know it's a stub model and won't be saving it
|
// after the the base model fetch, kv-v2 has a second associated
|
||||||
// because we don't have access to that endpoint
|
// version model that contains the secret data
|
||||||
isStub: true,
|
secretModel = await this.fetchV2Models(capabilities, secretModel, params);
|
||||||
});
|
}
|
||||||
let targetVersion = params.version ? parseInt(params.version, 10) : null;
|
return {
|
||||||
let versionId = targetVersion ? [backend, secret, targetVersion] : [backend, secret];
|
secret: secretModel,
|
||||||
return this.store
|
capabilities,
|
||||||
.findRecord('secret-v2-version', JSON.stringify(versionId), { reload: true })
|
};
|
||||||
.then(versionModel => {
|
|
||||||
secretModel.set('selectedVersion', versionModel);
|
|
||||||
return secretModel;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}),
|
|
||||||
capabilities: this.capabilities(secret),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ export default ApplicationSerializer.extend({
|
|||||||
},
|
},
|
||||||
serialize(snapshot) {
|
serialize(snapshot) {
|
||||||
let secret = snapshot.belongsTo('secret');
|
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;
|
version = version || 0;
|
||||||
return {
|
return {
|
||||||
data: snapshot.attr('secretData'),
|
data: snapshot.attr('secretData'),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="empty-state">
|
<div class="empty-state" ...attributes>
|
||||||
<div class="empty-state-content">
|
<div class="empty-state-content">
|
||||||
<h3 class="empty-state-title">
|
<h3 class="empty-state-title">
|
||||||
{{title}}
|
{{title}}
|
||||||
</h3>
|
</h3>
|
||||||
{{#if message}}
|
{{#if message}}
|
||||||
<p class="empty-state-message">
|
<p class="empty-state-message" data-test-empty-state-message>
|
||||||
{{message}}
|
{{message}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/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">
|
<div data-test-metadata-fields class="form-section box is-shadowless is-fullwidth">
|
||||||
<label class="title is-5">
|
<label class="title is-5">
|
||||||
Secret metadata
|
Secret metadata
|
||||||
@@ -9,6 +9,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/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}}
|
{{#if @showAdvancedMode}}
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<label class="title is-5">
|
<label class="title is-5">
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
</p.levelRight>
|
</p.levelRight>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div class="secret-control-bar">
|
<div class="secret-control-bar">
|
||||||
|
{{#unless (and (eq mode 'show') isWriteWithoutRead)}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
data-test-secret-json-toggle=true
|
data-test-secret-json-toggle=true
|
||||||
@@ -52,9 +53,10 @@
|
|||||||
/>
|
/>
|
||||||
<label for="json" class="has-text-grey">JSON</label>
|
<label for="json" class="has-text-grey">JSON</label>
|
||||||
</div>
|
</div>
|
||||||
|
{{/unless}}
|
||||||
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
||||||
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
|
{{#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">
|
<div class="control">
|
||||||
<BasicDropdown
|
<BasicDropdown
|
||||||
@class="popup-menu"
|
@class="popup-menu"
|
||||||
@@ -134,9 +136,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/let}}
|
{{/let}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (and (eq @mode "show") this.isV2)}}
|
{{#if (and (eq @mode "show") this.isV2 (not @model.failedServerRead))}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SecretVersionMenu @version={{this.modelForData}} />
|
<SecretVersionMenu
|
||||||
|
@version={{this.modelForData}}
|
||||||
|
@onRefresh={{action 'refresh'}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<BasicDropdown
|
<BasicDropdown
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="box is-sideless is-fullwidth is-marginless is-paddingless">
|
<div class="box is-sideless is-fullwidth is-marginless is-paddingless">
|
||||||
<MessageError @model={{model}} @errorMessage={{error}} />
|
<MessageError @model={{model}} @errorMessage={{error}} />
|
||||||
<NamespaceReminder @mode="edit" @noun="secret" />
|
<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">
|
<div class="form-section">
|
||||||
<AlertBanner
|
<AlertBanner
|
||||||
@type="warning"
|
@type="warning"
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
@secretData={{secretData}}
|
@secretData={{secretData}}
|
||||||
@isV2={{isV2}}
|
@isV2={{isV2}}
|
||||||
@canEditV2Secret={{canEditV2Secret}}
|
@canEditV2Secret={{canEditV2Secret}}
|
||||||
|
@showWriteWithoutReadWarning={{isWriteWithoutRead}}
|
||||||
@model={{model}}
|
@model={{model}}
|
||||||
@editActions={{hash
|
@editActions={{hash
|
||||||
codemirrorUpdated=(action "codemirrorUpdated")
|
codemirrorUpdated=(action "codemirrorUpdated")
|
||||||
|
|||||||
@@ -17,6 +17,16 @@
|
|||||||
Learn more
|
Learn more
|
||||||
</DocLink>
|
</DocLink>
|
||||||
</EmptyState>
|
</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}}
|
{{else}}
|
||||||
{{#if showAdvancedMode}}
|
{{#if showAdvancedMode}}
|
||||||
{{json-editor
|
{{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.secrets.length, 3, 'renders three secrets');
|
||||||
assert.equal(listPage.filterInputValue, 'filter/', 'pageFilter has been reset');
|
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'),
|
visitEditRoot: visitable('/vault/secrets/:backend/edit'),
|
||||||
toggleJSON: clickable('[data-test-secret-json-toggle]'),
|
toggleJSON: clickable('[data-test-secret-json-toggle]'),
|
||||||
hasMetadataFields: isPresent('[data-test-metadata-fields]'),
|
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: {
|
editor: {
|
||||||
fillIn: codeFillable('[data-test-component="json-editor"]'),
|
fillIn: codeFillable('[data-test-component="json-editor"]'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export default create({
|
|||||||
toggleIsPresent: isPresent('[data-test-secret-json-toggle]'),
|
toggleIsPresent: isPresent('[data-test-secret-json-toggle]'),
|
||||||
edit: clickable('[data-test-secret-edit]'),
|
edit: clickable('[data-test-secret-edit]'),
|
||||||
editIsPresent: isPresent('[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: {
|
editor: {
|
||||||
content: code('[data-test-component="json-editor"]'),
|
content: code('[data-test-component="json-editor"]'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ module('Unit | Adapter | secret-v2-version', function(hooks) {
|
|||||||
let fakeStore = {
|
let fakeStore = {
|
||||||
peekRecord() {
|
peekRecord() {
|
||||||
return {
|
return {
|
||||||
|
rollbackAttributes() {},
|
||||||
reload() {},
|
reload() {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user