KV search box when no list access to metadata (#12626)
* get credentials card test and setup * call getcrednetials card and remove path test error * configuration * metadata search box * changelog * checking if it is noReadAccess * try removing test * blah * a test * blah * stuff * attempting a clean up to solve issue * Another attempt * test1 * test2 * test3 * test4 * test5 * test6 * test7 * finally? * clean up
This commit is contained in:
parent
33cca7586a
commit
92223b600e
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Add KV secret search box when no metadata list access.
|
||||
```
|
|
@ -5,11 +5,16 @@
|
|||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <GetCredentialsCard @title="Get Credentials" @searchLabel="Role to use" @models={{array 'database/roles'}} />
|
||||
* <GetCredentialsCard @title="Get Credentials" @searchLabel="Role to use" @models={{array 'database/roles'}} @type="role" @backend={{model.backend}}/>
|
||||
* ```
|
||||
* @param title=null {String} - The title displays the card title
|
||||
* @param searchLabel=null {String} - The text above the searchSelect component
|
||||
* @param models=null {Array} - An array of model types to fetch from the API. Passed through to SearchSelect component
|
||||
* @param type=null {String} - Determines where the transitionTo goes. If role to backend.credentials, if secret backend.show
|
||||
* @param shouldUseFallback=[false] {Boolean} - If true the input is used instead of search select.
|
||||
* @param subText=[null] {String} - Text below title
|
||||
* @param placeHolder=[null] {String} - Only works if shouldUseFallback is true. Displays the helper text inside the input.
|
||||
* @param backend=null {String} - Name of the backend to look up on search.
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
@ -20,25 +25,36 @@ export default class GetCredentialsCard extends Component {
|
|||
@service router;
|
||||
@service store;
|
||||
@tracked role = '';
|
||||
@tracked secret = '';
|
||||
|
||||
@action
|
||||
async transitionToCredential() {
|
||||
const role = this.role;
|
||||
const secret = this.secret;
|
||||
if (role) {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.credentials', role);
|
||||
}
|
||||
if (secret) {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', secret);
|
||||
}
|
||||
}
|
||||
|
||||
get buttonDisabled() {
|
||||
return !this.role;
|
||||
return !this.role && !this.secret;
|
||||
}
|
||||
@action
|
||||
handleRoleInput(value) {
|
||||
// if it comes in from the fallback component then the value is a string otherwise it's an array
|
||||
let role = value;
|
||||
if (Array.isArray(value)) {
|
||||
role = value[0];
|
||||
if (this.args.type === 'role') {
|
||||
// if it comes in from the fallback component then the value is a string otherwise it's an array
|
||||
// which currently only happens if type is role.
|
||||
if (Array.isArray(value)) {
|
||||
this.role = value[0];
|
||||
} else {
|
||||
this.role = value;
|
||||
}
|
||||
}
|
||||
if (this.args.type === 'secret') {
|
||||
this.secret = value;
|
||||
}
|
||||
this.role = role;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
* @modelForData={{@modelForData}}
|
||||
* @isV2=true
|
||||
* @secretData={{@secretData}}
|
||||
* @canCreateSecretMetadata=true
|
||||
* @canCreateSecretMetadata=false
|
||||
* />
|
||||
* ```
|
||||
* @param {string} mode - create, edit, show determines what view to display
|
||||
|
@ -20,7 +20,7 @@
|
|||
* @param {object} modelForData - a class that helps track secret data, defined in secret-edit
|
||||
* @param {boolean} isV2 - whether or not KV1 or KV2
|
||||
* @param {object} secretData - class that is created in secret-edit
|
||||
* @param {boolean} canCreateSecretMetadata - based on permissions to the /metadata/ endpoint. If user has secret create access.
|
||||
* @param {boolean} canUpdateSecretMetadata - based on permissions to the /metadata/ endpoint. If user has secret update. create is not enough for metadata.
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
|
|
@ -109,7 +109,8 @@ export default Component.extend(FocusOnInsertMixin, WithNavToNearestAncestor, {
|
|||
'mode'
|
||||
),
|
||||
canDeleteSecretMetadata: alias('checkMetadataCapabilities.canDelete'),
|
||||
canCreateSecretMetadata: alias('checkMetadataCapabilities.canCreate'),
|
||||
canUpdateSecretMetadata: alias('checkMetadataCapabilities.canUpdate'),
|
||||
canReadSecretMetadata: alias('checkMetadataCapabilities.canRead'),
|
||||
|
||||
requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
|
||||
|
||||
|
|
|
@ -4,19 +4,31 @@ import Route from '@ember/routing/route';
|
|||
export default Route.extend({
|
||||
wizard: service(),
|
||||
store: service(),
|
||||
model() {
|
||||
async model() {
|
||||
let backend = this.modelFor('vault.cluster.secrets.backend');
|
||||
if (this.wizard.featureState === 'list') {
|
||||
this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', backend.get('type'));
|
||||
}
|
||||
if (backend.isV2KV) {
|
||||
// design wants specific default to show that can't be set in the model
|
||||
backend.set('casRequired', backend.casRequired ? backend.casRequired : 'False');
|
||||
backend.set(
|
||||
'deleteVersionAfter',
|
||||
backend.deleteVersionAfter !== '0s' ? backend.deleteVersionAfter : 'Never delete'
|
||||
);
|
||||
backend.set('maxVersions', backend.maxVersions ? backend.maxVersions : 'Not set');
|
||||
let canRead = await this.store
|
||||
.findRecord('capabilities', `${backend.id}/config`)
|
||||
.then(response => response.canRead);
|
||||
// only set these config params if they can read the config endpoint.
|
||||
if (canRead) {
|
||||
// design wants specific default to show that can't be set in the model
|
||||
backend.set('casRequired', backend.casRequired ? backend.casRequired : 'False');
|
||||
backend.set(
|
||||
'deleteVersionAfter',
|
||||
backend.deleteVersionAfter !== '0s' ? backend.deleteVersionAfter : 'Never delete'
|
||||
);
|
||||
backend.set('maxVersions', backend.maxVersions ? backend.maxVersions : 'Not set');
|
||||
} else {
|
||||
// remove the default values from the model if they don't have read access otherwise it will display the defaults even if they've been set (because they error on returning config data)
|
||||
// normally would catch the config error in the secret-v2 adapter, but I need the functions to proceed, not stop. So we remove the values here.
|
||||
backend.set('casRequired', null);
|
||||
backend.set('deleteVersionAfter', null);
|
||||
backend.set('maxVersions', null);
|
||||
}
|
||||
}
|
||||
return backend;
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ const SUPPORTED_BACKENDS = supportedSecretBackends();
|
|||
export default Route.extend({
|
||||
templateName: 'vault/cluster/secrets/backend/list',
|
||||
pathHelp: service('path-help'),
|
||||
noMetadataPermissions: false,
|
||||
queryParams: {
|
||||
page: {
|
||||
refreshModel: true,
|
||||
|
@ -111,6 +112,9 @@ export default Route.extend({
|
|||
// if we're at the root we don't want to throw
|
||||
if (backendModel && err.httpStatus === 404 && secret === '') {
|
||||
return [];
|
||||
} else if (backendModel.engineType === 'kv' && backendModel.isV2KV) {
|
||||
this.set('noMetadataPermissions', true);
|
||||
return [];
|
||||
} else {
|
||||
// else we're throwing and dealing with this in the error action
|
||||
throw err;
|
||||
|
@ -149,6 +153,7 @@ export default Route.extend({
|
|||
let backend = this.enginePathParam();
|
||||
let backendModel = this.store.peekRecord('secret-engine', backend);
|
||||
let has404 = this.has404;
|
||||
let noMetadataPermissions = this.noMetadataPermissions;
|
||||
// only clear store cache if this is a new model
|
||||
if (secret !== controller.get('baseKey.id')) {
|
||||
this.store.clearAllDatasets();
|
||||
|
@ -157,6 +162,7 @@ export default Route.extend({
|
|||
controller.setProperties({
|
||||
model,
|
||||
has404,
|
||||
noMetadataPermissions,
|
||||
backend,
|
||||
backendModel,
|
||||
baseKey: { id: secret },
|
||||
|
|
|
@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
|||
|
||||
export default class MetadataShow extends Route {
|
||||
@service store;
|
||||
noReadAccess = false;
|
||||
|
||||
beforeModel() {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
|
@ -11,14 +12,23 @@ export default class MetadataShow extends Route {
|
|||
|
||||
model(params) {
|
||||
let { secret } = params;
|
||||
return this.store.queryRecord('secret-v2', {
|
||||
backend: this.backend,
|
||||
id: secret,
|
||||
});
|
||||
return this.store
|
||||
.queryRecord('secret-v2', {
|
||||
backend: this.backend,
|
||||
id: secret,
|
||||
})
|
||||
.catch(error => {
|
||||
// there was an error likely in read metadata.
|
||||
// still load the page and handle what you show by filtering for this property
|
||||
if (error.httpStatus === 403) {
|
||||
this.noReadAccess = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set('backend', this.backend); // for backendCrumb
|
||||
controller.set('model', model);
|
||||
controller.set('noReadAccess', this.noReadAccess);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
<form class="selectable-card is-rounded no-flex">
|
||||
<form class="selectable-card is-rounded no-flex data-test-get-credentials-card">
|
||||
<div class="is-flex-between is-fullwidth card-details" >
|
||||
<h3 class="title is-5">{{@title}}</h3>
|
||||
</div>
|
||||
<div class="has-top-bottom-margin">
|
||||
<p class="is-label search-label">{{@searchLabel}}</p>
|
||||
</div>
|
||||
<p class="sub-text">{{@subText}}</p>
|
||||
<SearchSelect
|
||||
@id={{id}}
|
||||
@models={{@models}}
|
||||
@selectLimit='1'
|
||||
@backend={{@backend}}
|
||||
@fallbackComponent='input-search'
|
||||
@shouldUseFallback={{@shouldUseFallback}}
|
||||
@onChange={{action 'handleRoleInput' }}
|
||||
@inputValue={{get model valuePath}}
|
||||
@placeHolder={{@placeHolder}}
|
||||
data-test-search-roles
|
||||
/>
|
||||
<input
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
@type="text"
|
||||
@value={{this.searchInput}}
|
||||
{{on 'keyup' this.inputChanged}}
|
||||
@placeholder={{@placeHolder}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -97,7 +97,8 @@
|
|||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (and @isV2 @canCreateSecretMetadata) }}
|
||||
{{!-- must have UPDATE permissions to add secret metadata. Create only will not work --}}
|
||||
{{#if (and @isV2 @canUpdateSecretMetadata)}}
|
||||
<ToggleButton
|
||||
@class="is-block"
|
||||
@toggleAttr={{"showMetadata"}}
|
||||
|
@ -113,7 +114,7 @@
|
|||
@updateValidationErrorCount={{action "updateValidationErrorCount"}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
|
@ -135,6 +136,7 @@
|
|||
{{/if}}
|
||||
|
||||
{{#if (eq @mode "edit")}}
|
||||
{{!-- no metadata option because metadata is version agnostic --}}
|
||||
<form onsubmit={{action "createOrUpdateKey" "edit"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless padding-top">
|
||||
{{#if @model.canReadSecretData}}
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
Secret
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{#if model.canReadMetadata}}
|
||||
{{!-- must have read access to /metadata see tab or update to update metadata--}}
|
||||
{{#if (or this.canReadSecretMetadata this.canUpdateSecretMetadata)}}
|
||||
<LinkTo @route="vault.cluster.secrets.backend.metadata" @model={{key.id}} @tagName="li" @activeClass="is-active" data-test-secret-metadata-tab>
|
||||
<LinkTo @route="vault.cluster.secrets.backend.metadata">
|
||||
Metadata
|
||||
|
@ -65,7 +66,7 @@
|
|||
@isV2={{isV2}}
|
||||
@secretData={{secretData}}
|
||||
@buttonDisabled={{buttonDisabled}}
|
||||
@canCreateSecretMetadata={{canCreateSecretMetadata}}
|
||||
@canUpdateSecretMetadata={{canUpdateSecretMetadata}}
|
||||
/>
|
||||
{{else if (eq mode "show")}}
|
||||
<SecretFormShow
|
||||
|
|
|
@ -5,107 +5,122 @@
|
|||
@backendCrumb={{backendCrumb}}
|
||||
@filter={{filter}}
|
||||
/>
|
||||
|
||||
{{#with (options-for-backend backendType tab) as |options|}}
|
||||
{{#if (or model.meta.total (not isConfigurableTab))}}
|
||||
<Toolbar>
|
||||
{{#if model.meta.total}}
|
||||
<ToolbarFilters>
|
||||
<NavigateInput
|
||||
@enterpriseProduct="vault"
|
||||
@filterFocusDidChange={{action "setFilterFocus"}}
|
||||
@filterDidChange={{action "setFilter"}}
|
||||
@filter={{this.filter}}
|
||||
@filterMatchesKey={{filterMatchesKey}}
|
||||
@firstPartialMatch={{firstPartialMatch}}
|
||||
@baseKey={{get baseKey "id"}}
|
||||
@shouldNavigateTree={{options.navigateTree}}
|
||||
@placeholder={{options.searchPlaceholder}}
|
||||
@mode={{if (eq tab 'certs') 'secrets-cert' 'secrets'}}
|
||||
@data-test-nav-input={{true}}
|
||||
/>
|
||||
{{#if filterFocused}}
|
||||
{{#if filterMatchesKey}}
|
||||
{{#unless filterIsFolder}}
|
||||
{{#if this.noMetadataPermissions}}
|
||||
<div class="box is-fullwidth is-shadowless has-tall-padding">
|
||||
<div class="selectable-card-container one-card">
|
||||
<GetCredentialsCard
|
||||
@shouldUseFallback={{true}}
|
||||
@title="View secret"
|
||||
@searchLabel="Secret path"
|
||||
@subText="Type the path of the secret you want to read"
|
||||
@placeHolder="secret/"
|
||||
@backend="kv"
|
||||
@type="secret"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#with (options-for-backend backendType tab) as |options|}}
|
||||
{{#if (or model.meta.total (not isConfigurableTab))}}
|
||||
<Toolbar>
|
||||
{{#if model.meta.total}}
|
||||
<ToolbarFilters>
|
||||
<NavigateInput
|
||||
@enterpriseProduct="vault"
|
||||
@filterFocusDidChange={{action "setFilterFocus"}}
|
||||
@filterDidChange={{action "setFilter"}}
|
||||
@filter={{this.filter}}
|
||||
@filterMatchesKey={{filterMatchesKey}}
|
||||
@firstPartialMatch={{firstPartialMatch}}
|
||||
@baseKey={{get baseKey "id"}}
|
||||
@shouldNavigateTree={{options.navigateTree}}
|
||||
@placeholder={{options.searchPlaceholder}}
|
||||
@mode={{if (eq tab 'certs') 'secrets-cert' 'secrets'}}
|
||||
@data-test-nav-input={{true}}
|
||||
/>
|
||||
{{#if filterFocused}}
|
||||
{{#if filterMatchesKey}}
|
||||
{{#unless filterIsFolder}}
|
||||
<p class="input-hint">
|
||||
<kbd>Enter</kbd> to view {{filter}}
|
||||
</p>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{#if firstPartialMatch}}
|
||||
<p class="input-hint">
|
||||
<kbd>Enter</kbd> to view {{filter}}
|
||||
<kbd>Tab</kbd> to autocomplete
|
||||
</p>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if firstPartialMatch}}
|
||||
<p class="input-hint">
|
||||
<kbd>Tab</kbd> to autocomplete
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
{{/if}}
|
||||
|
||||
<ToolbarActions>
|
||||
<ToolbarSecretLink
|
||||
@secret=''
|
||||
@mode="create"
|
||||
@type="add"
|
||||
@queryParams={{query-params initialKey=(or filter baseKey.id) itemType=tab}}
|
||||
@data-test-secret-create=true
|
||||
>
|
||||
{{options.create}}
|
||||
</ToolbarSecretLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.meta.total}}
|
||||
{{#each model as |item|}}
|
||||
{{!-- Because of the component helper cannot use glimmer nested SecretList::Item --}}
|
||||
{{#let (component options.listItemPartial) as |Component|}}
|
||||
<Component
|
||||
@item={{item}}
|
||||
@backendModel={{backendModel}}
|
||||
@backendType={{backendType}}
|
||||
@delete={{action "delete" item "secret"}}
|
||||
@itemPath={{concat options.modelPrefix item.id}}
|
||||
@itemType={{options.item}}
|
||||
@modelType={{@modelType}}
|
||||
@options={{options}}
|
||||
@toggleZeroAddress={{action "toggleZeroAddress" item backendModel}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{else}}
|
||||
<div class="box is-sideless">
|
||||
{{#if filterFocused}}
|
||||
There are no {{pluralize options.item}} matching <code>{{filter}}</code>, press <kbd>ENTER</kbd> to add one.
|
||||
{{else}}
|
||||
There are no {{pluralize options.item}} matching <code>{{filter}}</code>.
|
||||
</ToolbarFilters>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#if (gt model.meta.lastPage 1) }}
|
||||
<ListPagination @page={{model.meta.currentPage}} @lastPage={{model.meta.lastPage}} @link={{concat "vault.cluster.secrets.backend.list" (unless baseKey.id "-root")}} @model={{compact (array backend (if baseKey.id baseKey.id))}} />
|
||||
|
||||
<ToolbarActions>
|
||||
<ToolbarSecretLink
|
||||
@secret=''
|
||||
@mode="create"
|
||||
@type="add"
|
||||
@queryParams={{query-params initialKey=(or filter baseKey.id) itemType=tab}}
|
||||
@data-test-secret-create=true
|
||||
>
|
||||
{{options.create}}
|
||||
</ToolbarSecretLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq baseKey.id '')}}
|
||||
<EmptyState
|
||||
@title="No {{pluralize options.item}} in this backend"
|
||||
@message="Secrets in this backend will be listed here. Add a secret to get started."
|
||||
>
|
||||
<SecretLink @mode="create" @secret="" @queryParams={{query-params initialKey=(or filter baseKey.id) itemType=tab}} @class="link">
|
||||
{{options.create}}
|
||||
</SecretLink>
|
||||
</EmptyState>
|
||||
|
||||
{{#if model.meta.total}}
|
||||
{{#each model as |item|}}
|
||||
{{!-- Because of the component helper cannot use glimmer nested SecretList::Item --}}
|
||||
{{#let (component options.listItemPartial) as |Component|}}
|
||||
<Component
|
||||
@item={{item}}
|
||||
@backendModel={{backendModel}}
|
||||
@backendType={{backendType}}
|
||||
@delete={{action "delete" item "secret"}}
|
||||
@itemPath={{concat options.modelPrefix item.id}}
|
||||
@itemType={{options.item}}
|
||||
@modelType={{@modelType}}
|
||||
@options={{options}}
|
||||
@toggleZeroAddress={{action "toggleZeroAddress" item backendModel}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{else}}
|
||||
{{#if filterIsFolder}}
|
||||
<div class="box is-sideless">
|
||||
{{#if filterFocused}}
|
||||
There are no {{pluralize options.item}} matching <code>{{filter}}</code>, press <kbd>ENTER</kbd> to add one.
|
||||
{{else}}
|
||||
There are no {{pluralize options.item}} matching <code>{{filter}}</code>.
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#if (gt model.meta.lastPage 1) }}
|
||||
<ListPagination @page={{model.meta.currentPage}} @lastPage={{model.meta.lastPage}} @link={{concat "vault.cluster.secrets.backend.list" (unless baseKey.id "-root")}} @model={{compact (array backend (if baseKey.id baseKey.id))}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq baseKey.id '')}}
|
||||
<EmptyState
|
||||
@title={{if (eq filter baseKey.id)
|
||||
(concat
|
||||
"No " (pluralize options.item) " under "" this.filter """
|
||||
)
|
||||
(concat
|
||||
"No folders matching "" this.filter """
|
||||
)
|
||||
}}
|
||||
/>
|
||||
@title="No {{pluralize options.item}} in this backend"
|
||||
@message="Secrets in this backend will be listed here. Add a secret to get started."
|
||||
>
|
||||
<SecretLink @mode="create" @secret="" @queryParams={{query-params initialKey=(or filter baseKey.id) itemType=tab}} @class="link">
|
||||
{{options.create}}
|
||||
</SecretLink>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
{{#if filterIsFolder}}
|
||||
<EmptyState
|
||||
@title={{if (eq filter baseKey.id)
|
||||
(concat
|
||||
"No " (pluralize options.item) " under "" this.filter """
|
||||
)
|
||||
(concat
|
||||
"No folders matching "" this.filter """
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/with}}
|
||||
{{/if}}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
</div>
|
||||
|
||||
<Toolbar>
|
||||
{{!-- You must have update on metadata, create is not enough. --}}
|
||||
{{#if this.model.canUpdateMetadata}}
|
||||
<ToolbarActions>
|
||||
<ToolbarLink @params={{array 'vault.cluster.secrets.backend.edit-metadata' this.model.id }}>
|
||||
|
@ -50,28 +51,38 @@
|
|||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each-in this.model.customMetadata as | key value|}}
|
||||
<InfoTableRow @alwaysRender={{false}} @label={{key}} @value={{value}} />
|
||||
{{else if this.noReadAccess}}
|
||||
<EmptyState
|
||||
@title="You do not have access to read secret metadata"
|
||||
@bottomBorder={{true}}
|
||||
@message="In order to edit secret metadata access, the UI requires read permissions; otherwise, data may be deleted. Edits can still be made via the API and CLI.">
|
||||
</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.">
|
||||
<LinkTo
|
||||
@route="vault.cluster.secrets.backend.edit-metadata"
|
||||
@model={{this.model.id}}
|
||||
data-test-add-custom-metadata
|
||||
>
|
||||
Add metadata
|
||||
</LinkTo>
|
||||
{{#if this.model.canUpdateMetadata}}
|
||||
<LinkTo
|
||||
@route="vault.cluster.secrets.backend.edit-metadata"
|
||||
@model={{this.model.id}}
|
||||
data-test-add-custom-metadata
|
||||
>
|
||||
Add metadata
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label class="title has-padding-top is-5">
|
||||
Secret Metadata
|
||||
</label>
|
||||
</div>
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Maximum versions" @value={{this.model.maxVersions}} />
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Check-and-Set required" @value={{this.model.casRequired}} />
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Delete version after" @value={{if (eq this.model.deleteVersionAfter "0s") "Never delete" this.model.deleteVersionAfter}} />
|
||||
</div>
|
||||
{{#unless this.noReadAccess}}
|
||||
<div class="form-section">
|
||||
<label class="title has-padding-top is-5">
|
||||
Secret Metadata
|
||||
</label>
|
||||
</div>
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Maximum versions" @value={{this.model.maxVersions}} />
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Check-and-Set required" @value={{this.model.casRequired}} />
|
||||
<InfoTableRow @alwaysRender={{true}} @label="Delete version after" @value={{if (eq this.model.deleteVersionAfter "0s") "Never delete" this.model.deleteVersionAfter}} />
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
@actionText="Configure new"
|
||||
@actionTo="vault.cluster.secrets.backend.create-root"
|
||||
@queryParam={{'connection'}}
|
||||
@type="role"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (or model.roleCapabilities.canList model.staticRoleCapabilities.canList) }}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
onChange=(action "onChange")
|
||||
inputValue=inputValue
|
||||
helpText=helpText
|
||||
placeHolder=placeHolder
|
||||
}}
|
||||
{{else}}
|
||||
<label class="{{if labelClass labelClass 'title is-4'}}" data-test-field-label>
|
||||
|
|
|
@ -395,34 +395,113 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('version 2 with restricted policy still allows creation', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
const V2_POLICY = `
|
||||
path "kv-v2/metadata/*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv-v2/data/secret" {
|
||||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
`;
|
||||
await consoleComponent.runCommands([
|
||||
`write sys/mounts/${backend} type=kv options=version=2`,
|
||||
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
||||
// delete any kv previously written here so that tests can be re-run
|
||||
'delete kv-v2/metadata/secret',
|
||||
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
||||
]);
|
||||
|
||||
let userToken = consoleComponent.lastLogOutput;
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
|
||||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
test('paths are properly encoded', async function(assert) {
|
||||
let backend = 'kv';
|
||||
let paths = [
|
||||
'(',
|
||||
')',
|
||||
'"',
|
||||
//"'",
|
||||
'!',
|
||||
'#',
|
||||
'$',
|
||||
'&',
|
||||
'*',
|
||||
'+',
|
||||
'@',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
'[',
|
||||
'\\',
|
||||
']',
|
||||
'^',
|
||||
'_',
|
||||
].map(char => `${char}some`);
|
||||
assert.expect(paths.length * 2);
|
||||
let secretName = '2';
|
||||
let commands = paths.map(path => `write '${backend}/${path}/${secretName}' 3=4`);
|
||||
await consoleComponent.runCommands(['write sys/mounts/kv type=kv', ...commands]);
|
||||
for (let path of paths) {
|
||||
await listPage.visit({ backend, id: path });
|
||||
assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`);
|
||||
await listPage.secrets.filterBy('text', '2')[0].click();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.show',
|
||||
`${path}: show page renders correctly`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('version 2 with restricted policy still allows edit but does not show metadata tab', async function(assert) {
|
||||
test('create secret with space shows version data', async function(assert) {
|
||||
let enginePath = `kv-${new Date().getTime()}`;
|
||||
let secretPath = 'space space';
|
||||
// mount version 2
|
||||
await mountSecrets.visit();
|
||||
await mountSecrets.selectType('kv');
|
||||
await mountSecrets
|
||||
.next()
|
||||
.path(enginePath)
|
||||
.submit();
|
||||
await settled();
|
||||
await listPage.create();
|
||||
await editPage.createSecret(secretPath, 'foo', 'bar');
|
||||
await settled();
|
||||
await click('[data-test-popup-menu-trigger="version"]');
|
||||
await settled();
|
||||
await click('[data-test-version-history]');
|
||||
await settled();
|
||||
assert.dom('[data-test-list-item-content]').exists('renders the version and not an error state');
|
||||
// click on version
|
||||
await click('[data-test-popup-menu-trigger="true"]');
|
||||
await click('[data-test-version]');
|
||||
await settled();
|
||||
// perform encode function that should be done by the encodePath
|
||||
let encodedSecretPath = secretPath.replace(/ /g, '%20');
|
||||
assert.equal(currentURL(), `/vault/secrets/${enginePath}/show/${encodedSecretPath}?version=1`);
|
||||
});
|
||||
|
||||
// the web cli does not handle a quote as part of a path, so we test it here via the UI
|
||||
test('creating a secret with a single or double quote works properly', async function(assert) {
|
||||
await consoleComponent.runCommands('write sys/mounts/kv type=kv');
|
||||
let paths = ["'some", '"some'];
|
||||
for (let path of paths) {
|
||||
await listPage.visitRoot({ backend: 'kv' });
|
||||
await listPage.create();
|
||||
await editPage.createSecret(`${path}/2`, 'foo', 'bar');
|
||||
await listPage.visit({ backend: 'kv', id: path });
|
||||
assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`);
|
||||
await listPage.secrets.filterBy('text', '2')[0].click();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.show',
|
||||
`${path}: show page renders correctly`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('filter clears on nav', async function(assert) {
|
||||
await consoleComponent.runCommands([
|
||||
'vault write sys/mounts/test type=kv',
|
||||
'refresh',
|
||||
'vault write test/filter/foo keys=a keys=b',
|
||||
'vault write test/filter/foo1 keys=a keys=b',
|
||||
'vault write test/filter/foo2 keys=a keys=b',
|
||||
]);
|
||||
await listPage.visit({ backend: 'test', id: 'filter' });
|
||||
assert.equal(listPage.secrets.length, 3, 'renders three secrets');
|
||||
await listPage.filterInput('filter/foo1');
|
||||
assert.equal(listPage.secrets.length, 1, 'renders only one secret');
|
||||
await listPage.secrets.objectAt(0).click();
|
||||
await showPage.breadcrumbs.filterBy('text', 'filter')[0].click();
|
||||
assert.equal(listPage.secrets.length, 3, 'renders three secrets');
|
||||
assert.equal(listPage.filterInputValue, 'filter/', 'pageFilter has been reset');
|
||||
});
|
||||
|
||||
// All policy test below this line
|
||||
test('version 2 with restricted policy still allows creation and does not show metadata tab', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
const V2_POLICY = `
|
||||
path "kv-v2/metadata/*" {
|
||||
|
@ -441,20 +520,72 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
|||
]);
|
||||
|
||||
let userToken = consoleComponent.lastLogOutput;
|
||||
// check secret edit
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await editPage.visitEdit({ backend, id: 'secret' });
|
||||
await editPage.editSecret('bar', 'baz');
|
||||
await settled();
|
||||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
|
||||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
//check for metadata tab
|
||||
//check for metadata tab which should not show because you don't have read capabilities
|
||||
assert.dom('[data-test-secret-metadata-tab]').doesNotExist('does not show metadata tab');
|
||||
});
|
||||
|
||||
test('version 2: with metadata no read or list access but access to the data endpoint', async function(assert) {
|
||||
let backend = 'no-metadata-read';
|
||||
let V2_POLICY_NO_LIST = `
|
||||
path "${backend}/metadata/*" {
|
||||
capabilities = ["create","update"]
|
||||
}
|
||||
path "${backend}/data/*" {
|
||||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
`;
|
||||
await consoleComponent.runCommands([
|
||||
// delete any kv previously written here so that tests can be re-run
|
||||
`delete ${backend}/metadata/secret-path`,
|
||||
// delete any previous mount with same name
|
||||
`delete sys/mounts/${backend}`,
|
||||
`write sys/mounts/${backend} type=kv options=version=2`,
|
||||
`write sys/policies/acl/metadata-no-list policy=${btoa(V2_POLICY_NO_LIST)}`,
|
||||
'write -field=client_token auth/token/create policies=metadata-no-list',
|
||||
]);
|
||||
await settled();
|
||||
let userToken2 = consoleComponent.lastLogOutput;
|
||||
await settled();
|
||||
await listPage.visitRoot({ backend });
|
||||
await settled();
|
||||
await listPage.create();
|
||||
await settled();
|
||||
await editPage.createSecretWithMetadata('secret-path', 'secret-key', 'secret-value', 101);
|
||||
await settled();
|
||||
await logout.visit();
|
||||
await settled();
|
||||
await authPage.login(userToken2);
|
||||
await settled();
|
||||
// test if metadata tab there and error and no edit. and you can’t see metadata that was setup.
|
||||
await click(`[data-test-auth-backend-link=${backend}]`);
|
||||
await settled();
|
||||
// this fails in IE11 on browserstack so going directly to URL
|
||||
// let card = document.querySelector('[data-test-search-roles]').childNodes[1];
|
||||
// await typeIn(card.querySelector('input'), 'secret-path');
|
||||
// await settled();
|
||||
await visit('/vault/secrets/no-metadata-read/show/secret-path');
|
||||
// await click('[data-test-get-credentials]');
|
||||
await settled();
|
||||
await assert
|
||||
.dom('[data-test-value-div="secret-key"]')
|
||||
.exists('secret view page and info table row with secret-key value');
|
||||
// check metadata
|
||||
await click('[data-test-secret-metadata-tab]');
|
||||
await settled();
|
||||
assert
|
||||
.dom('[data-test-empty-state-message]')
|
||||
.hasText(
|
||||
'In order to edit secret metadata access, the UI requires read permissions; otherwise, data may be deleted. Edits can still be made via the API and CLI.'
|
||||
);
|
||||
});
|
||||
|
||||
// KV delete operations testing
|
||||
test('version 2 with policy with destroy capabilities shows modal', async function(assert) {
|
||||
let backend = 'kv-v2';
|
||||
const V2_POLICY = `
|
||||
|
@ -582,111 +713,7 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
|||
await writeSecret(backend, 'secret', 'foo', 'bar');
|
||||
assert.dom('[data-test-secret-v2-delete="true"]').exists('drop down delete shows');
|
||||
});
|
||||
|
||||
test('paths are properly encoded', async function(assert) {
|
||||
let backend = 'kv';
|
||||
let paths = [
|
||||
'(',
|
||||
')',
|
||||
'"',
|
||||
//"'",
|
||||
'!',
|
||||
'#',
|
||||
'$',
|
||||
'&',
|
||||
'*',
|
||||
'+',
|
||||
'@',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
'[',
|
||||
'\\',
|
||||
']',
|
||||
'^',
|
||||
'_',
|
||||
].map(char => `${char}some`);
|
||||
assert.expect(paths.length * 2);
|
||||
let secretName = '2';
|
||||
let commands = paths.map(path => `write '${backend}/${path}/${secretName}' 3=4`);
|
||||
await consoleComponent.runCommands(['write sys/mounts/kv type=kv', ...commands]);
|
||||
for (let path of paths) {
|
||||
await listPage.visit({ backend, id: path });
|
||||
assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`);
|
||||
await listPage.secrets.filterBy('text', '2')[0].click();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.show',
|
||||
`${path}: show page renders correctly`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('create secret with space shows version data', async function(assert) {
|
||||
let enginePath = `kv-${new Date().getTime()}`;
|
||||
let secretPath = 'space space';
|
||||
// mount version 2
|
||||
await mountSecrets.visit();
|
||||
await mountSecrets.selectType('kv');
|
||||
await mountSecrets
|
||||
.next()
|
||||
.path(enginePath)
|
||||
.submit();
|
||||
await settled();
|
||||
await listPage.create();
|
||||
await editPage.createSecret(secretPath, 'foo', 'bar');
|
||||
await settled();
|
||||
await click('[data-test-popup-menu-trigger="version"]');
|
||||
await settled();
|
||||
await click('[data-test-version-history]');
|
||||
await settled();
|
||||
assert.dom('[data-test-list-item-content]').exists('renders the version and not an error state');
|
||||
// click on version
|
||||
await click('[data-test-popup-menu-trigger="true"]');
|
||||
await click('[data-test-version]');
|
||||
await settled();
|
||||
// perform encode function that should be done by the encodePath
|
||||
let encodedSecretPath = secretPath.replace(/ /g, '%20');
|
||||
assert.equal(currentURL(), `/vault/secrets/${enginePath}/show/${encodedSecretPath}?version=1`);
|
||||
});
|
||||
|
||||
// the web cli does not handle a quote as part of a path, so we test it here via the UI
|
||||
test('creating a secret with a single or double quote works properly', async function(assert) {
|
||||
await consoleComponent.runCommands('write sys/mounts/kv type=kv');
|
||||
let paths = ["'some", '"some'];
|
||||
for (let path of paths) {
|
||||
await listPage.visitRoot({ backend: 'kv' });
|
||||
await listPage.create();
|
||||
await editPage.createSecret(`${path}/2`, 'foo', 'bar');
|
||||
await listPage.visit({ backend: 'kv', id: path });
|
||||
assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`);
|
||||
await listPage.secrets.filterBy('text', '2')[0].click();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.show',
|
||||
`${path}: show page renders correctly`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('filter clears on nav', async function(assert) {
|
||||
await consoleComponent.runCommands([
|
||||
'vault write sys/mounts/test type=kv',
|
||||
'refresh',
|
||||
'vault write test/filter/foo keys=a keys=b',
|
||||
'vault write test/filter/foo1 keys=a keys=b',
|
||||
'vault write test/filter/foo2 keys=a keys=b',
|
||||
]);
|
||||
await listPage.visit({ backend: 'test', id: 'filter' });
|
||||
assert.equal(listPage.secrets.length, 3, 'renders three secrets');
|
||||
await listPage.filterInput('filter/foo1');
|
||||
assert.equal(listPage.secrets.length, 1, 'renders only one secret');
|
||||
await listPage.secrets.objectAt(0).click();
|
||||
await showPage.breadcrumbs.filterBy('text', 'filter')[0].click();
|
||||
assert.equal(listPage.secrets.length, 3, 'renders three secrets');
|
||||
assert.equal(listPage.filterInputValue, 'filter/', 'pageFilter has been reset');
|
||||
});
|
||||
// end of KV delete operation testing
|
||||
|
||||
let setupNoRead = async function(backend, canReadMeta = false) {
|
||||
const V2_WRITE_ONLY_POLICY = `
|
||||
|
|
|
@ -147,49 +147,4 @@ module('Acceptance | settings/mount-secret-backend', function(hooks) {
|
|||
await settled();
|
||||
assert.dom('[data-test-row-value="Maximum number of versions"]').hasText('Not set');
|
||||
});
|
||||
|
||||
test('version 2 with no create to sys/mounts endpoint does not allows mounting of secret engine', async function(assert) {
|
||||
let backend = `kv-noMount-${new Date().getTime()}`;
|
||||
const V2_POLICY = `
|
||||
path "${backend}/*" {
|
||||
capabilities = ["update","list","create","read","sudo","delete"]
|
||||
}
|
||||
path "sys/mounts/*"
|
||||
{
|
||||
capabilities = ["read", "delete", "list", "sudo"]
|
||||
}
|
||||
|
||||
# List existing secrets engines.
|
||||
path "sys/mounts"
|
||||
{
|
||||
capabilities = ["read"]
|
||||
}
|
||||
`;
|
||||
await consoleComponent.runCommands([
|
||||
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
||||
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
||||
]);
|
||||
|
||||
let userToken = consoleComponent.lastLogOutput;
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
// create the engine
|
||||
await mountSecrets.visit();
|
||||
await mountSecrets.selectType('kv');
|
||||
await mountSecrets
|
||||
.next()
|
||||
.path(backend)
|
||||
.setMaxVersion(101)
|
||||
.submit();
|
||||
await settled();
|
||||
assert.ok(
|
||||
find('[data-test-flash-message]').textContent.trim(),
|
||||
`You do not have access to the sys/mounts endpoint. The secret engine was not mounted.`
|
||||
);
|
||||
assert.equal(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
|
||||
|
||||
await page.secretList();
|
||||
await settled();
|
||||
assert.dom(`[data-test-secret-backend-row=${backend}]`).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { module, test } from 'qunit';
|
|||
import { run } from '@ember/runloop';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import Service from '@ember/service';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { render, typeIn } from '@ember/test-helpers';
|
||||
import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
|
@ -45,9 +45,23 @@ module('Integration | Component | get-credentials-card', function(hooks) {
|
|||
test('it shows button that can be clicked to credentials route when an item is selected', async function(assert) {
|
||||
const models = ['database/role'];
|
||||
this.set('models', models);
|
||||
await render(hbs`<GetCredentialsCard @title={{title}} @searchLabel={{searchLabel}} @models={{models}}/>`);
|
||||
await render(
|
||||
hbs`<GetCredentialsCard @title={{title}} @searchLabel={{searchLabel}} @models={{models}} @type="role"/>`
|
||||
);
|
||||
await clickTrigger();
|
||||
await selectChoose('', 'my-role');
|
||||
assert.dom('[data-test-get-credentials]').isEnabled();
|
||||
});
|
||||
|
||||
test('it shows input field that can be clicked to a secret when role is secret', async function(assert) {
|
||||
await render(
|
||||
hbs`<GetCredentialsCard @title={{title}} @shouldUseFallback={{true}} @placeHolder="secret/" @backend="kv" @type="secret"/>`
|
||||
);
|
||||
let card = document.querySelector('[data-test-search-roles]').childNodes[1];
|
||||
let placeholder = card.querySelector('input').placeholder;
|
||||
assert.equal(placeholder, 'secret/');
|
||||
|
||||
await typeIn(card.querySelector('input'), 'test');
|
||||
assert.dom('[data-test-get-credentials]').isEnabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,14 @@ export default create({
|
|||
.secretValue(value)
|
||||
.save();
|
||||
},
|
||||
createSecretWithMetadata: async function(path, key, value, maxVersion) {
|
||||
return this.path(path)
|
||||
.secretKey(key)
|
||||
.secretValue(value)
|
||||
.toggleMetadata()
|
||||
.maxVersion(maxVersion)
|
||||
.save();
|
||||
},
|
||||
editSecret: async function(key, value) {
|
||||
return this.secretKey(key)
|
||||
.secretValue(value)
|
||||
|
|
Loading…
Reference in New Issue