mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
Restructure SSH and AWS configuration screens (#27831)
* setup the toggle to display mount configuration options * whew.. getting there. aws only, borked for ssh * another round, better than before * masked things * changelog * fix broken oss test * move to component * handle ssh things and cleanup * wip test coverage * test coverage for the component * copywrite header miss * update no model error * setup configuration aws acceptance tests * update CONFIURABLE_SECRET_ENGINES * acceptance tests for aws * ssh configuration * clean up * remove comment * move to confirm model before destructuring * pr comments * fix check for ssh config error * add message check in api error test * pr comments
This commit is contained in:
parent
d75aee21b8
commit
1f982bf13a
15 changed files with 559 additions and 24 deletions
3
changelog/27831.txt
Normal file
3
changelog/27831.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: For AWS and SSH secret engines hide mount configuration details in toggle and display configuration details or cta.
|
||||
```
|
||||
20
ui/app/adapters/aws/root-config.js
Normal file
20
ui/app/adapters/aws/root-config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationAdapter from '../application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default class AwsRootConfig extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
// For now this is only being used on the vault.cluster.secrets.backend.configuration route. This is a read-only route.
|
||||
// Eventually, this will be used to create the root config for the AWS secret backend, replacing the requests located on the secret-engine adapter.
|
||||
queryRecord(store, type, query) {
|
||||
const { backend } = query;
|
||||
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/config/root`, 'GET').then((resp) => {
|
||||
resp.id = backend;
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
}
|
||||
20
ui/app/adapters/ssh/ca-config.js
Normal file
20
ui/app/adapters/ssh/ca-config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationAdapter from '../application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default class SshCaConfig extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
// For now this is only being used on the vault.cluster.secrets.backend.configuration route. This is a read-only route.
|
||||
// Eventually, this will be used to create the ca config for the SSH secret backend, replacing the requests located on the secret-engine adapter.
|
||||
queryRecord(store, type, query) {
|
||||
const { backend } = query;
|
||||
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/config/ca`, 'GET').then((resp) => {
|
||||
resp.id = backend;
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
}
|
||||
44
ui/app/components/secret-engine/configuration-details.hbs
Normal file
44
ui/app/components/secret-engine/configuration-details.hbs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#if this.configError}}
|
||||
{{! Surface API errors not associated with empty configuration details }}
|
||||
<Page::Error @error={{this.configError}} />
|
||||
{{else if this.configModel}}
|
||||
{{#each this.configModel.attrs as |attr|}}
|
||||
{{#if attr.options.sensitive}}
|
||||
<InfoTableRow
|
||||
alwaysRender={{not (is-empty-value (get @model attr.name))}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{get this.configModel (or attr.options.fieldValue attr.name)}}
|
||||
>
|
||||
{{#if attr.options.sensitive}}
|
||||
<MaskedInput @value={{get @model attr.name}} @name={{attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
|
||||
{{/if}}
|
||||
</InfoTableRow>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@alwaysRender={{not (is-empty-value (get @model attr.name))}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{get this.configModel (or attr.options.fieldValue attr.name)}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{! Prompt for a user to configure the secret engine }}
|
||||
<EmptyState
|
||||
data-test-config-cta
|
||||
@title="{{this.typeDisplay}} not configured"
|
||||
@message="Get started by configuring your {{this.typeDisplay}} engine."
|
||||
>
|
||||
<Hds::Link::Standalone
|
||||
@icon="chevron-right"
|
||||
@iconPosition="trailing"
|
||||
@text="Configure {{this.typeDisplay}}"
|
||||
@route="vault.cluster.settings.configure-secret-backend"
|
||||
@model={{@model.id}}
|
||||
/>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
94
ui/app/components/secret-engine/configuration-details.ts
Normal file
94
ui/app/components/secret-engine/configuration-details.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { allEngines } from 'vault/helpers/mountable-secret-engines';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type AdapterError from '@ember-data/adapter';
|
||||
import type Model from '@ember-data/model';
|
||||
|
||||
/**
|
||||
* @module ConfigurationDetails
|
||||
* `ConfigurationDetails` is used by configurable secret engines (AWS, SSH) to show either an API error, configuration details, or a prompt to configure the engine. Which of these is shown is determined by the engine type and whether the user has configured the engine yet.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <SecretEngine::ConfigurationDetails @model={{this.model}} />
|
||||
* ```
|
||||
*
|
||||
* @param {object} model - The secret-engine model to be configured.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: SecretEngineModel | null;
|
||||
}
|
||||
|
||||
interface ConfigError {
|
||||
httpStatus: number | null;
|
||||
message: string | null;
|
||||
errors: object | null;
|
||||
}
|
||||
|
||||
export default class ConfigurationDetails extends Component<Args> {
|
||||
@service declare readonly store: Store;
|
||||
@tracked configError: ConfigError | null = null;
|
||||
@tracked configModel: Model | null = null;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
const { model } = this.args;
|
||||
// Should not be able to get here without a model, but in case an upstream change allows it, handle the error higher up.
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const { id, type } = model;
|
||||
// Fetch the config for the engine type.
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
this.fetchAwsRootConfig(id);
|
||||
break;
|
||||
case 'ssh':
|
||||
this.fetchSshCaConfig(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAwsRootConfig(backend: string) {
|
||||
try {
|
||||
this.configModel = await this.store.queryRecord('aws/root-config', { backend });
|
||||
} catch (e: AdapterError) {
|
||||
// If the error is something other than 404 "not found" then an API error has come back and this will be displayed to the user as an error.
|
||||
// If it's 404 then configError is not set nor is the configModel and a prompt to configure will be shown.
|
||||
if (e.httpStatus !== 404) {
|
||||
this.configError = e;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSshCaConfig(backend: string) {
|
||||
try {
|
||||
this.configModel = await this.store.queryRecord('ssh/ca-config', { backend });
|
||||
} catch (e: AdapterError) {
|
||||
// The SSH api does not return a 404 not found but a 400 error after first mounting the engine with the
|
||||
// message that keys have not been configured yet.
|
||||
// We need to check the message of the 400 error and if it's the keys message, return a prompt instead of a configError.
|
||||
if (e.httpStatus !== 404 && e.errors[0] !== `keys haven't been configured yet`) {
|
||||
this.configError = e;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
get typeDisplay() {
|
||||
if (!this.args.model) return;
|
||||
const { type } = this.args.model;
|
||||
return allEngines().find((engine) => engine.type === type)?.displayName;
|
||||
}
|
||||
}
|
||||
32
ui/app/models/aws/root-config.js
Normal file
32
ui/app/models/aws/root-config.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
export default class AwsRootConfig extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string') accessKey;
|
||||
@attr('string') region;
|
||||
@attr('string', {
|
||||
label: 'IAM endpoint',
|
||||
})
|
||||
iamEndpoint;
|
||||
@attr('string', {
|
||||
label: 'STS endpoint',
|
||||
})
|
||||
stsEndpoint;
|
||||
@attr('number', {
|
||||
defaultValue: -1,
|
||||
label: 'Maximum retries',
|
||||
subText: 'Number of max retries the client should use for recoverable errors. Default is -1.',
|
||||
})
|
||||
maxRetries;
|
||||
// there are more options available on the API, but the UI does not support them yet.
|
||||
get attrs() {
|
||||
const keys = ['accessKey', 'region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}
|
||||
}
|
||||
20
ui/app/models/ssh/ca-config.js
Normal file
20
ui/app/models/ssh/ca-config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
export default class SshCaConfig extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string', { sensitive: true }) privateKey; // obfuscated, never returned by API
|
||||
@attr('string', { sensitive: true }) publicKey;
|
||||
@attr('boolean', { defaultValue: true })
|
||||
generateSigningKey;
|
||||
// there are more options available on the API, but the UI does not support them yet.
|
||||
get attrs() {
|
||||
const keys = ['publicKey', 'generateSigningKey'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,23 +26,28 @@
|
|||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.model.attrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
<InfoTableRow
|
||||
@alwaysRender={{not (is-empty-value (get this.model attr.name))}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{stringify (get this.model (or attr.options.fieldValue attr.name))}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@alwaysRender={{and (not (is-empty-value (get this.model attr.name))) (not-eq attr.name "version")}}
|
||||
@formatTtl={{eq attr.options.editType "ttl"}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{get this.model (or attr.options.fieldValue attr.name)}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<SecretEngine::ConfigurationDetails @model={{this.model}} />
|
||||
|
||||
<SecretsEngineMountConfig @model={{this.model}} class="has-top-margin-xl has-bottom-margin-xl" data-test-mount-config />
|
||||
|
||||
{{else}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.model.attrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
<InfoTableRow
|
||||
@alwaysRender={{not (is-empty-value (get this.model attr.name))}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{stringify (get this.model (or attr.options.fieldValue attr.name))}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@alwaysRender={{and (not (is-empty-value (get this.model attr.name))) (not-eq attr.name "version")}}
|
||||
@formatTtl={{eq attr.options.editType "ttl"}}
|
||||
@label={{or attr.options.label (to-label attr.name)}}
|
||||
@value={{get this.model (or attr.options.fieldValue attr.name)}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
@ -8,17 +8,17 @@
|
|||
data-test-page-error
|
||||
>
|
||||
{{#if (eq @error.httpStatus 404)}}
|
||||
<h1 class="title is-3 has-text-grey-light">
|
||||
<h1 class="title is-3 has-text-grey-light" data-test-page-error-title={{@error.httpStatus}}>
|
||||
404 Not Found
|
||||
</h1>
|
||||
<p>Sorry, we were unable to find any content at <code>{{@error.path}}</code>.</p>
|
||||
{{else if (eq @error.httpStatus 403)}}
|
||||
<h1 class="title is-3 has-text-grey-light">
|
||||
<h1 class="title is-3 has-text-grey-light" data-test-page-error-title={{@error.httpStatus}}>
|
||||
Not authorized
|
||||
</h1>
|
||||
<p>You are not authorized to access content at <code>{{@error.path}}</code>.</p>
|
||||
{{else}}
|
||||
<h1 class="title is-3 has-text-grey-light">
|
||||
<h1 class="title is-3 has-text-grey-light" data-test-page-error-title={{@error.httpStatus}}>
|
||||
Error
|
||||
</h1>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, currentURL } from '@ember/test-helpers';
|
||||
import { click, fillIn, visit, currentURL } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -15,6 +15,11 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
|||
import { runCmd } from 'vault/tests/helpers/commands';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import {
|
||||
createConfig,
|
||||
expectedConfigKeys,
|
||||
expectedValueOfConfigKeys,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
|
||||
module('Acceptance | aws | configuration', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
|
@ -30,6 +35,20 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||
return authPage.login();
|
||||
});
|
||||
|
||||
test('it should prompt configuration after mounting the aws engine', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
// in this test go through the full mount process. Bypass this step in later tests.
|
||||
await visit('/vault/settings/mount-secret-backend');
|
||||
await click(SES.mountType('aws'));
|
||||
await fillIn(GENERAL.inputByAttr('path'), path);
|
||||
await click(SES.mountSubmit);
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('AWS not configured');
|
||||
assert.dom(GENERAL.emptyStateActions).hasText('Configure AWS');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should transition to configure page on Configure click from toolbar', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
|
@ -93,4 +112,62 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it show AWS configuration details', async function (assert) {
|
||||
assert.expect(12);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type, attributes: payload } };
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
createConfig(this.store, path, type); // create the aws root config in the store
|
||||
await click(SES.configTab);
|
||||
for (const key of expectedConfigKeys(type)) {
|
||||
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `value for ${key} on the ${type} config details exists.`);
|
||||
}
|
||||
// check mount configuration details are present and accurate.
|
||||
await click(SES.configurationToggle);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Path'))
|
||||
.hasText(`${path}/`, 'mount path is displayed in the configuration details');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should update AWS configuration details after editing', async function (assert) {
|
||||
assert.expect(4);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
// create accessKey with value foo and confirm it shows up in the details page.
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
|
||||
await click(GENERAL.saveButtonId('root'));
|
||||
await click(SES.viewBackend);
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access key is foo');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Region'))
|
||||
.doesNotExist('Region has not been added therefor it does not show up on the details view.');
|
||||
// edit accessKey and another field and confirm the details page is updated.
|
||||
await click(SES.configure);
|
||||
await fillIn(GENERAL.inputByAttr('accessKey'), 'hello');
|
||||
await click(GENERAL.menuTrigger);
|
||||
await fillIn(GENERAL.selectByAttr('region'), 'ca-central-1');
|
||||
await click(GENERAL.saveButtonId('root'));
|
||||
await click(SES.viewBackend);
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('hello', 'Access key has been updated to hello');
|
||||
assert.dom(GENERAL.infoRowValue('Region')).hasText('ca-central-1', 'Region has been added');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, currentURL, waitFor } from '@ember/test-helpers';
|
||||
import { click, fillIn, currentURL, waitFor, visit } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -11,6 +11,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import authPage from 'vault/tests/pages/auth';
|
||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { runCmd } from 'vault/tests/helpers/commands';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
|
||||
module('Acceptance | ssh | configuration', function (hooks) {
|
||||
|
|
@ -21,6 +22,20 @@ module('Acceptance | ssh | configuration', function (hooks) {
|
|||
return authPage.login();
|
||||
});
|
||||
|
||||
test('it should prompt configuration after mounting ssh engine', async function (assert) {
|
||||
const sshPath = `ssh-${this.uid}`;
|
||||
// in this test go through the full mount process. Bypass this step in later tests.
|
||||
await visit('/vault/settings/mount-secret-backend');
|
||||
await click(SES.mountType('ssh'));
|
||||
await fillIn(GENERAL.inputByAttr('path'), sshPath);
|
||||
await click(SES.mountSubmit);
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('SSH not configured');
|
||||
assert.dom(GENERAL.emptyStateActions).hasText('Configure SSH');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${sshPath}`);
|
||||
});
|
||||
|
||||
test('it should show a public key after saving default configuration', async function (assert) {
|
||||
const sshPath = `ssh-${this.uid}`;
|
||||
await enablePage.enable('ssh', sshPath);
|
||||
|
|
@ -44,6 +59,15 @@ module('Acceptance | ssh | configuration', function (hooks) {
|
|||
await waitFor(SES.ssh.sshInput('public-key'));
|
||||
assert.dom(SES.ssh.sshInput('public-key')).exists('renders the public key input on form page');
|
||||
assert.dom(SES.ssh.sshInput('public-key')).hasClass('masked-input', 'public key is masked');
|
||||
|
||||
await click(SES.viewBackend);
|
||||
await click(SES.configTab);
|
||||
assert
|
||||
.dom(`[data-test-value-div="Public key"] [data-test-masked-input]`)
|
||||
.hasText('***********', 'value for Public key is on config details and is masked');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Generate signing key'))
|
||||
.hasText('Yes', 'value for Generate signing key displays default of true/yes.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${sshPath}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ export const GENERAL = {
|
|||
validation: (attr: string) => `[data-test-field-validation=${attr}]`,
|
||||
validationWarning: (attr: string) => `[data-test-validation-warning=${attr}]`,
|
||||
messageError: '[data-test-message-error]',
|
||||
pageError: {
|
||||
error: '[data-test-page-error]',
|
||||
errorTitle: (httpStatus: number) => `[data-test-page-error-title="${httpStatus}"]`,
|
||||
errorMessage: '[data-test-page-error-message]',
|
||||
errorDetails: '[data-test-page-error-details]',
|
||||
},
|
||||
kvObjectEditor: {
|
||||
deleteRow: (idx = 0) => `[data-test-kv-delete-row="${idx}"]`,
|
||||
},
|
||||
|
|
|
|||
107
ui/tests/helpers/secret-engine/secret-engine-helpers.js
Normal file
107
ui/tests/helpers/secret-engine/secret-engine-helpers.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export const createSecretsEngine = (store, type, path) => {
|
||||
store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
id: path,
|
||||
path: `${path}/`,
|
||||
type: type,
|
||||
data: {
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
return store.peekRecord('secret-engine', path);
|
||||
};
|
||||
|
||||
const createAwsRootConfig = (store, backend) => {
|
||||
store.pushPayload('aws/root-config', {
|
||||
id: backend,
|
||||
modelName: 'aws/root-config',
|
||||
data: {
|
||||
backend: backend,
|
||||
region: 'us-west-2',
|
||||
access_key: '123-key',
|
||||
iam_endpoint: 'iam-endpoint',
|
||||
sts_endpoint: 'sts-endpoint',
|
||||
},
|
||||
});
|
||||
return store.peekRecord('aws/root-config', backend);
|
||||
};
|
||||
|
||||
const createSshCaConfig = (store, backend) => {
|
||||
store.pushPayload('ssh/ca-config', {
|
||||
id: backend,
|
||||
modelName: 'ssh/ca-config',
|
||||
data: {
|
||||
backend: backend,
|
||||
public_key: 'public-key',
|
||||
generate_signing_key: true,
|
||||
},
|
||||
});
|
||||
return store.peekRecord('ssh/ca-config', backend);
|
||||
};
|
||||
|
||||
export function configUrl(type, backend) {
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
return `${backend}/config/root`;
|
||||
case 'ssh':
|
||||
return `/${backend}/config/ca`;
|
||||
default:
|
||||
return `/${backend}/config`;
|
||||
}
|
||||
}
|
||||
// send the type of config you want and the name of the backend path to push the config to the store.
|
||||
export const createConfig = (store, backend, type) => {
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
return createAwsRootConfig(store, backend);
|
||||
case 'ssh':
|
||||
return createSshCaConfig(store, backend);
|
||||
}
|
||||
};
|
||||
// Used in tests to assert the expected keys in the config details of configurable secret engines
|
||||
export const expectedConfigKeys = (type) => {
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
return ['Access key', 'Region', 'IAM endpoint', 'STS endpoint', 'Maximum retries'];
|
||||
case 'ssh':
|
||||
return ['Public key', 'Generate signing key'];
|
||||
}
|
||||
};
|
||||
|
||||
const valueOfAwsKeys = (string) => {
|
||||
switch (string) {
|
||||
case 'Access key':
|
||||
return '123-key';
|
||||
case 'Region':
|
||||
return 'us-west-2';
|
||||
case 'IAM endpoint':
|
||||
return 'iam-endpoint';
|
||||
case 'STS endpoint':
|
||||
return 'sts-endpoint';
|
||||
case 'Maximum retries':
|
||||
return '-1';
|
||||
}
|
||||
};
|
||||
|
||||
const valueOfSshKeys = (string) => {
|
||||
switch (string) {
|
||||
case 'Public key':
|
||||
return '***********';
|
||||
case 'Generate signing key':
|
||||
return 'Yes';
|
||||
}
|
||||
};
|
||||
// Used in tests to assert the expected values in the config details of configurable secret engines
|
||||
export const expectedValueOfConfigKeys = (type, string) => {
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
return valueOfAwsKeys(string);
|
||||
case 'ssh':
|
||||
return valueOfSshKeys(string);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { overrideResponse, allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
|
||||
import {
|
||||
createSecretsEngine,
|
||||
createConfig,
|
||||
configUrl,
|
||||
expectedConfigKeys,
|
||||
expectedValueOfConfigKeys,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
|
||||
module('Integration | Component | SecretEngine::configuration-details', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
this.store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('it shows prompt message if no config is returned', async function (assert) {
|
||||
assert.expect(CONFIGURABLE_SECRET_ENGINES.length * 2);
|
||||
for (const type of CONFIGURABLE_SECRET_ENGINES) {
|
||||
const title = type.toUpperCase();
|
||||
const backend = `test-404-${type}`;
|
||||
this.model = createSecretsEngine(this.store, type, backend);
|
||||
this.server.get(configUrl(type, backend), () => {
|
||||
return overrideResponse(404);
|
||||
});
|
||||
|
||||
await render(hbs`<SecretEngine::ConfigurationDetails @model={{this.model}}/>`);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText(`${title} not configured`);
|
||||
assert.dom(GENERAL.emptyStateMessage).hasText(`Get started by configuring your ${title} engine.`);
|
||||
}
|
||||
});
|
||||
|
||||
test('it shows API error', async function (assert) {
|
||||
assert.expect(CONFIGURABLE_SECRET_ENGINES.length * 2);
|
||||
for (const type of CONFIGURABLE_SECRET_ENGINES) {
|
||||
const backend = `test-400-${type}`;
|
||||
this.model = createSecretsEngine(this.store, type, backend);
|
||||
this.server.get(configUrl(type, backend), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
|
||||
await render(hbs`<SecretEngine::ConfigurationDetails @model={{this.model}}/>`);
|
||||
assert.dom(GENERAL.pageError.errorTitle(400)).hasText('Error');
|
||||
assert.dom(GENERAL.pageError.errorDetails).hasText('bad request');
|
||||
}
|
||||
});
|
||||
|
||||
test('it shows config details if config data is returned', async function (assert) {
|
||||
assert.expect(14);
|
||||
for (const type of CONFIGURABLE_SECRET_ENGINES) {
|
||||
const backend = `test-${type}`;
|
||||
this.model = createSecretsEngine(this.store, type, backend);
|
||||
createConfig(this.store, backend, type);
|
||||
this.server.get(configUrl(type, backend), () => {
|
||||
return overrideResponse(200);
|
||||
});
|
||||
|
||||
await render(hbs`<SecretEngine::ConfigurationDetails @model={{this.model}}/>`);
|
||||
for (const key of expectedConfigKeys(type)) {
|
||||
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `${key} value for the ${type} config details exists.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
1
ui/types/vault/models/secret-engine.d.ts
vendored
1
ui/types/vault/models/secret-engine.d.ts
vendored
|
|
@ -9,6 +9,7 @@ import type { ModelValidations, FormField, FormFieldGroups } from 'vault/app-typ
|
|||
import type MountConfigModel from 'vault/models/mount-config';
|
||||
|
||||
export default class SecretEngineModel extends Model {
|
||||
id: string;
|
||||
path: string;
|
||||
type: string;
|
||||
description: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue