UI: PKI Sign Intermediate (#18842)

This commit is contained in:
Chelsea Shaw 2023-01-27 12:07:55 -06:00 committed by GitHub
parent 419a92a632
commit 8788317b8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 567 additions and 30 deletions

3
changelog/18842.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
**New PKI UI**: Add beta support for new and improved PKI UI
```

View File

@ -24,7 +24,7 @@ export default class PkiActionAdapter extends ApplicationAdapter {
? `${baseUrl}/issuers/generate/intermediate/${type}` ? `${baseUrl}/issuers/generate/intermediate/${type}`
: `${baseUrl}/intermediate/generate/${type}`; : `${baseUrl}/intermediate/generate/${type}`;
case 'sign-intermediate': case 'sign-intermediate':
return `${baseUrl}/issuer/${issuerName}/sign-intermediate`; return `${baseUrl}/issuer/${encodePath(issuerName)}/sign-intermediate`;
default: default:
assert('actionType must be one of import, generate-root, generate-csr or sign-intermediate'); assert('actionType must be one of import, generate-root, generate-csr or sign-intermediate');
} }

View File

@ -0,0 +1,19 @@
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ApplicationAdapter from '../application';
export default class PkiSignIntermediateAdapter extends ApplicationAdapter {
namespace = 'v1';
createRecord(store, type, snapshot) {
const serializer = store.serializerFor(type.modelName);
const { backend, issuerRef } = snapshot.record;
const url = `${this.buildURL()}/${encodePath(backend)}/issuer/${encodePath(issuerRef)}/sign-intermediate`;
const data = serializer.serialize(snapshot, type);
return this.ajax(url, 'POST', { data }).then((result) => ({
// sign-intermediate can happen multiple times per issuer,
// so the ID needs to be unique from the issuer ID
id: result.request_id,
...result,
}));
}
}

View File

@ -37,11 +37,11 @@ export default class PkiCertificateBaseModel extends Model {
@attr('string') commonName; @attr('string') commonName;
// Attrs that come back from API POST request // Attrs that come back from API POST request
@attr() caChain; @attr({ masked: true, label: 'CA Chain' }) caChain;
@attr('string', { masked: true }) certificate; @attr('string', { masked: true }) certificate;
@attr('number') expiration; @attr('number') expiration;
@attr('number', { formatDate: true }) revocationTime; @attr('number', { formatDate: true }) revocationTime;
@attr('string') issuingCa; @attr('string', { label: 'Issuing CA', masked: true }) issuingCa;
@attr('string') privateKey; @attr('string') privateKey;
@attr('string') privateKeyType; @attr('string') privateKeyType;
@attr('string') serialNumber; @attr('string') serialNumber;

View File

@ -32,8 +32,6 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
@attr isDefault; // readonly @attr isDefault; // readonly
@attr('string') issuerId; @attr('string') issuerId;
@attr('string', { displayType: 'masked' }) certificate;
@attr('string', { displayType: 'masked', label: 'CA Chain' }) caChain;
@attr('string', { @attr('string', {
label: 'Default key ID', label: 'Default key ID',

View File

@ -0,0 +1,97 @@
import { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';
import { withModelValidations } from 'vault/decorators/model-validations';
import PkiCertificateBaseModel from './certificate/base';
const validations = {
csr: [{ type: 'presence', message: 'CSR is required.' }],
};
@withModelValidations(validations)
@withFormFields([
'csr',
'useCsrValues',
'commonName',
'customTtl',
'notBeforeDuration',
'format',
'permittedDnsDomains',
'maxPathLength',
])
export default class PkiSignIntermediateModel extends PkiCertificateBaseModel {
getHelpUrl(backend) {
return `/v1/${backend}/issuer/example/sign-intermediate?help=1`;
}
@attr issuerRef;
@attr('string', {
label: 'CSR',
editType: 'textarea',
subText: 'The PEM-encoded CSR to be signed.',
})
csr;
@attr('boolean', {
label: 'Use CSR values',
subText:
'Subject information and key usages specified in the CSR will be used over parameters provided here, and extensions in the CSR will be copied into the issued certificate.',
docLink: '/vault/api-docs/secret/pki#use_csr_values',
})
useCsrValues;
@attr({
label: 'Not valid after',
detailsLabel: 'Issued certificates expire after',
subText:
'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date.',
editType: 'yield',
})
customTtl;
@attr({
label: 'Backdate validity',
detailsLabel: 'Issued certificate backdating',
helperTextDisabled: 'Vault will use the default value, 30s',
helperTextEnabled:
'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.',
editType: 'ttl',
defaultValue: '30s',
})
notBeforeDuration;
@attr('string')
commonName;
@attr({
label: 'Permitted DNS domains',
subText:
'DNS domains for which certificates are allowed to be issued or signed by this CA certificate. Enter each value as a new input.',
})
permittedDnsDomains;
@attr({
subText: 'Specifies the maximum path length to encode in the generated certificate. -1 means no limit',
defaultValue: '-1',
})
maxPathLength;
/* Signing Options overrides */
@attr({
label: 'Use PSS',
subText:
'If checked, PSS signatures will be used over PKCS#1v1.5 signatures when a RSA-type issuer is used. Ignored for ECDSA/Ed25519 issuers.',
})
usePss;
@attr({
label: 'Subject Key Identifier (SKID)',
subText:
'Value for the subject key identifier, specified as a string in hex format. If this is empty, Vault will automatically calculate the SKID. ',
})
skid;
@attr({
possibleValues: ['0', '256', '384', '512'],
})
signatureBits;
}

View File

@ -1,3 +1,7 @@
import ApplicationSerializer from '../application'; import ApplicationSerializer from '../application';
export default class PkiRoleSerializer extends ApplicationSerializer {} export default class PkiRoleSerializer extends ApplicationSerializer {
attrs = {
name: { serialize: false },
};
}

View File

@ -237,8 +237,11 @@
id={{@attr.name}} id={{@attr.name}}
value={{or (get @model this.valuePath) @attr.options.defaultValue}} value={{or (get @model this.valuePath) @attr.options.defaultValue}}
oninput={{this.onChangeWithEvent}} oninput={{this.onChangeWithEvent}}
class="textarea" class="textarea {{if this.validationError 'has-error-border'}}"
></textarea> ></textarea>
{{#if this.validationError}}
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
{{/if}}
{{else if (eq @attr.options.editType "password")}} {{else if (eq @attr.options.editType "password")}}
<Input <Input
data-test-input={{@attr.name}} data-test-input={{@attr.name}}

View File

@ -1,10 +1,10 @@
<Toolbar> <Toolbar>
<ToolbarActions> <ToolbarActions>
{{#if @canRotate}} {{!-- {{#if @canRotate}}
<ToolbarLink @route="issuers.generate-root" @type="rotate-cw" @issuer={{@issuer.id}} data-test-pki-issuer-rotate-root> <ToolbarLink @route="issuers.generate-root" @type="rotate-cw" @issuer={{@issuer.id}} data-test-pki-issuer-rotate-root>
Rotate this root Rotate this root
</ToolbarLink> </ToolbarLink>
{{/if}} {{/if}} --}}
{{#if @canCrossSign}} {{#if @canCrossSign}}
<ToolbarLink <ToolbarLink
@route="issuers.issuer.cross-sign" @route="issuers.issuer.cross-sign"
@ -20,16 +20,52 @@
Sign Intermediate Sign Intermediate
</ToolbarLink> </ToolbarLink>
{{/if}} {{/if}}
<DownloadButton <BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
class="toolbar-link" <D.Trigger
@filename={{@issuer.id}} data-test-popup-menu-trigger="true"
@data={{@issuer.certificate}} class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@extension="pem" @htmlTag="button"
data-test-issuer-download data-test-issuer-download
> >
Download Download
<Chevron @direction="down" @isButton={{true}} /> <Chevron @direction="down" @isButton={{true}} />
</DownloadButton> </D.Trigger>
<D.Content @defaultClass="popup-menu-content">
<nav class="box menu" aria-label="snapshots actions">
<ul class="menu-list">
{{#if @pem}}
{{! should never be null, but if it is we don't want to let users download an empty file }}
<li class="action">
<DownloadButton
class="link"
@filename={{@issuer.id}}
@data={{@pem}}
@extension="pem"
data-test-issuer-download-type="pem"
>
PEM format
</DownloadButton>
</li>
{{/if}}
{{#if @der}}
{{! should never be null, but if it is we don't want to let users download an empty file }}
<li class="action">
<DownloadButton
class="link"
@filename={{@issuer.id}}
@data={{@der}}
@extension="der"
data-test-issuer-download-type="der"
>
DER format
</DownloadButton>
</li>
{{/if}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
{{#if @canConfigure}} {{#if @canConfigure}}
<ToolbarLink @route="issuers.issuer.edit" @issuer={{@issuer.id}} data-test-pki-issuer-configure> <ToolbarLink @route="issuers.issuer.edit" @issuer={{@issuer.id}} data-test-pki-issuer-configure>
Configure Configure
@ -58,7 +94,7 @@
</h2> </h2>
{{/if}} {{/if}}
{{#each fields as |attr|}} {{#each fields as |attr|}}
{{#if (eq attr.options.displayType "masked")}} {{#if attr.options.masked}}
<InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @issuer attr.name}}> <InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @issuer attr.name}}>
<MaskedInput <MaskedInput
@name={{or attr.options.label (humanize (dasherize attr.name))}} @name={{or attr.options.label (humanize (dasherize attr.name))}}

View File

@ -6,6 +6,7 @@ import PkiActionModel from 'vault/models/pki/action';
interface Args { interface Args {
model: PkiActionModel; model: PkiActionModel;
groups: Map<[key: string], Array<string>> | null;
} }
export default class PkiGenerateToggleGroupsComponent extends Component<Args> { export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
@ -21,6 +22,7 @@ export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
} }
get groups() { get groups() {
if (this.args.groups) return this.args.groups;
const groups = { const groups = {
'Key parameters': this.keyParamFields, 'Key parameters': this.keyParamFields,
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'], 'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],

View File

@ -0,0 +1,81 @@
{{#if @model.id}}
{{! Model only has ID once form has been submitted and saved }}
<Toolbar />
<main data-test-sign-intermediate-result>
<div class="box is-sideless is-fullwidth is-shadowless">
<AlertBanner
@title="Next steps"
@type="warning"
@message="The CA Chain and Issuing CA values will only be available once. Make sure you copy and save it now."
/>
{{#each this.showFields as |fieldName|}}
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
<InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @issuer attr.name}}>
{{#if (and attr.options.masked (get @model attr.name))}}
<MaskedInput @value={{get @model attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
{{else if (eq attr.name "serialNumber")}}
<LinkTo
@route="certificates.certificate.details"
@model={{@model.serialNumber}}
>{{@model.serialNumber}}</LinkTo>
{{else}}
<Icon @name="minus" />
{{/if}}
</InfoTableRow>
{{/let}}
{{/each}}
</div>
</main>
{{else}}
<form {{on "submit" (perform this.save)}} data-test-sign-intermediate-form>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<NamespaceReminder @mode={{"create"}} @noun="signed intermediate" />
{{#each @model.formFields as |attr|}}
<FormField
data-test-field={{attr}}
@attr={{attr}}
@model={{@model}}
@modelValidations={{this.modelValidations}}
@showHelpText={{false}}
>
{{! attr customTtl has editType yield and will show this component }}
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
</FormField>
{{/each}}
<PkiGenerateToggleGroups @model={{@model}} @groups={{this.groups}} />
</div>
<div class="has-top-padding-s">
<button
type="submit"
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
disabled={{this.save.isRunning}}
data-test-pki-sign-intermediate-save
>
Save
</button>
<button
type="button"
class="button has-left-margin-s"
disabled={{this.save.isRunning}}
{{on "click" this.cancel}}
data-test-pki-sign-intermediate-cancel
>
Cancel
</button>
{{#if this.inlineFormAlert}}
<div class="control">
<AlertInline
@type="danger"
@paddingTop={{true}}
@message={{this.inlineFormAlert}}
@mimicRefresh={{true}}
data-test-form-error
/>
</div>
{{/if}}
</div>
</form>
{{/if}}

View File

@ -0,0 +1,62 @@
import { action } from '@ember/object';
import { service } from '@ember/service';
import { waitFor } from '@ember/test-waiters';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import PkiIssuerModel from 'vault/models/pki/issuer';
import FlashMessageService from 'vault/services/flash-messages';
import errorMessage from 'vault/utils/error-message';
interface Args {
onCancel: CallableFunction;
model: PkiIssuerModel;
}
export default class PkiSignIntermediateFormComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@tracked errorBanner = '';
@tracked inlineFormAlert = '';
@tracked modelValidations = null;
@action cancel() {
this.args.model.unloadRecord();
this.args.onCancel();
}
@task
@waitFor
*save(event: Event) {
event.preventDefault();
const { isValid, state, invalidFormMessage } = this.args.model.validate();
this.modelValidations = isValid ? null : state;
this.inlineFormAlert = invalidFormMessage;
if (!isValid) return;
try {
yield this.args.model.save();
this.flashMessages.success('Successfully signed CSR.');
} catch (e) {
this.errorBanner = errorMessage(e);
this.inlineFormAlert = 'There was a problem signing the CSR.';
}
}
get groups() {
return {
'Signing options': ['usePss', 'skid', 'signatureBits'],
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],
'Additional subject fields': [
'ou',
'organization',
'country',
'locality',
'province',
'streetAddress',
'postalCode',
],
};
}
get showFields() {
return ['serialNumber', 'certificate', 'issuingCa', 'caChain'];
}
}

View File

@ -2,8 +2,30 @@ import PkiIssuerIndexRoute from './index';
export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute { export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute {
// Details route gets issuer data from PkiIssuerIndexRoute // Details route gets issuer data from PkiIssuerIndexRoute
setupController(controller, resolvedModel) { async setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel); super.setupController(controller, resolvedModel);
controller.breadcrumbs.push({ label: resolvedModel.id }); controller.breadcrumbs.push({ label: resolvedModel.id });
const pem = await this.fetchCertByFormat(resolvedModel.id, 'pem');
const der = await this.fetchCertByFormat(resolvedModel.id, 'der');
controller.pem = pem;
controller.der = der;
}
/**
* @private fetches cert by format so it's available for download
*/
fetchCertByFormat(issuerId, format) {
const endpoint = `/v1/${this.secretMountPath.currentPath}/issuer/${issuerId}/${format}`;
const adapter = this.store.adapterFor('application');
try {
return adapter.rawRequest(endpoint, 'GET', { unauthenticated: true }).then(function (response) {
if (format === 'der') {
return response.blob();
}
return response.text();
});
} catch (e) {
return null;
}
} }
} }

View File

@ -1,3 +1,29 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class PkiIssuerSignRoute extends Route {} export default class PkiIssuerSignRoute extends Route {
@service store;
@service secretMountPath;
@service pathHelp;
beforeModel() {
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record.
return this.pathHelp.getNewModel('pki/sign-intermediate', this.secretMountPath.currentPath);
}
model() {
const { issuer_ref } = this.paramsFor('issuers/issuer');
return this.store.createRecord('pki/sign-intermediate', { issuerRef: issuer_ref });
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'issuers', route: 'issuers.index' },
{ label: resolvedModel.issuerRef, route: 'issuers.issuer.details' },
{ label: 'sign intermediate' },
];
}
}

View File

@ -9,8 +9,11 @@
</h1> </h1>
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>
<Page::PkiIssuerDetails <Page::PkiIssuerDetails
@issuer={{this.model}} @issuer={{this.model}}
@pem={{this.pem}}
@der={{this.der}}
@canRotate={{this.model.canRotateIssuer}} @canRotate={{this.model.canRotateIssuer}}
@canCrossSign={{this.model.canCrossSign}} @canCrossSign={{this.model.canCrossSign}}
@canSignIntermediate={{this.model.canSignIntermediate}} @canSignIntermediate={{this.model.canSignIntermediate}}

View File

@ -1 +1,16 @@
route: issuers.issuer.sign <PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-issuer-page-title>
<Icon @name="file-text" @size="24" class="has-text-grey-light" />
Sign intermediate
</h1>
</p.levelLeft>
</PageHeader>
<PkiSignIntermediateForm
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.issuer.details" model=this.model.issuerRef}}
/>

View File

@ -0,0 +1,97 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, fillIn, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import Sinon from 'sinon';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
const selectors = {
form: '[data-test-sign-intermediate-form]',
csrInput: '[data-test-input="csr"]',
toggleSigningOptions: '[data-test-toggle-group="Signing options"]',
toggleSANOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
toggleAdditionalFields: '[data-test-toggle-group="Additional subject fields"]',
fieldByName: (name) => `[data-test-field="${name}"]`,
saveButton: '[data-test-pki-sign-intermediate-save]',
cancelButton: '[data-test-pki-sign-intermediate-cancel]',
fieldError: '[data-test-inline-alert]',
formError: '[data-test-form-error]',
resultsContainer: '[data-test-sign-intermediate-result]',
rowByName: (name) => `[data-test-row-label="${name}"]`,
valueByName: (name) => `[data-test-value-div="${name}"]`,
};
module('Integration | Component | pki-sign-intermediate-form', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
hooks.beforeEach(async function () {
this.store = this.owner.lookup('service:store');
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = 'pki-test';
this.model = this.store.createRecord('pki/sign-intermediate', { issuerRef: 'some-issuer' });
this.onCancel = Sinon.spy();
});
test('renders correctly on load', async function (assert) {
assert.expect(9);
await render(hbs`<PkiSignIntermediateForm @onCancel={{this.onCancel}} @model={{this.model}} />`, {
owner: this.engine,
});
assert.dom(selectors.form).exists('Form is rendered');
assert.dom(selectors.resultsContainer).doesNotExist('Results display not rendered');
assert.dom('[data-test-field]').exists({ count: 8 }, '8 default fields shown');
assert.dom(selectors.toggleSigningOptions).exists();
assert.dom(selectors.toggleSANOptions).exists();
assert.dom(selectors.toggleAdditionalFields).exists();
await click(selectors.toggleSigningOptions);
['usePss', 'skid', 'signatureBits'].forEach((name) => {
assert.dom(selectors.fieldByName(name)).exists();
});
});
test('it shows the returned values on successful save', async function (assert) {
assert.expect(13);
await render(hbs`<PkiSignIntermediateForm @onCancel={{this.onCancel}} @model={{this.model}} />`, {
owner: this.engine,
});
this.server.post(`/pki-test/issuer/some-issuer/sign-intermediate`, function (schema, req) {
const payload = JSON.parse(req.requestBody);
assert.strictEqual(payload.csr, 'example-data', 'Request made to correct endpoint on save');
return {
request_id: 'some-id',
data: {
serial_number: '31:52:b9:09:40',
ca_chain: ['-----root pem------'],
issuing_ca: '-----issuing ca------',
certificate: '-----certificate------',
},
};
});
await click(selectors.saveButton);
assert.dom(selectors.formError).hasText('There is an error with this form.', 'Shows validation errors');
assert.dom(selectors.csrInput).hasClass('has-error-border');
assert.dom(selectors.fieldError).hasText('CSR is required.');
await fillIn(selectors.csrInput, 'example-data');
await click(selectors.saveButton);
[
{ label: 'Serial number' },
{ label: 'CA Chain', masked: true },
{ label: 'Certificate', masked: true },
{ label: 'Issuing CA', masked: true },
].forEach(({ label, masked }) => {
assert.dom(selectors.rowByName(label)).exists();
if (masked) {
assert.dom(selectors.valueByName(label)).hasText('***********', `${label} is masked`);
} else {
assert.dom(selectors.valueByName(label)).hasText('31:52:b9:09:40', `Renders ${label}`);
assert.dom(`${selectors.valueByName(label)} a`).exists(`${label} is a link`);
}
});
});
});

View File

@ -8,6 +8,7 @@ const selectors = {
keys: '[data-test-toggle-group="Key parameters"]', keys: '[data-test-toggle-group="Key parameters"]',
sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]', sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
subjectFields: '[data-test-toggle-group="Additional subject fields"]', subjectFields: '[data-test-toggle-group="Additional subject fields"]',
toggleByName: (name) => `[data-test-toggle-group="${name}"]`,
}; };
module('Integration | Component | PkiGenerateToggleGroups', function (hooks) { module('Integration | Component | PkiGenerateToggleGroups', function (hooks) {
@ -94,4 +95,32 @@ module('Integration | Component | PkiGenerateToggleGroups', function (hooks) {
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`); assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
}); });
}); });
test('it should render groups according to the passed @groups', async function (assert) {
assert.expect(11);
const fieldsA = ['ou', 'organization'];
const fieldsZ = ['country', 'locality', 'province', 'streetAddress', 'postalCode'];
this.set('groups', {
'Group A': fieldsA,
'Group Z': fieldsZ,
});
await render(hbs`<PkiGenerateToggleGroups @model={{this.model}} @groups={{this.groups}} />`, {
owner: this.engine,
});
assert.dom(selectors.toggleByName('Group A')).hasText('Group A', 'First group renders');
assert.dom(selectors.toggleByName('Group Z')).hasText('Group Z', 'Second group renders');
await click(selectors.toggleByName('Group A'));
assert.dom('[data-test-field]').exists({ count: fieldsA.length }, 'Correct number of fields render');
fieldsA.forEach((key) => {
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
});
await click(selectors.toggleByName('Group Z'));
assert.dom('[data-test-field]').exists({ count: fieldsZ.length }, 'Correct number of fields render');
fieldsZ.forEach((key) => {
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
});
});
}); });

View File

@ -38,7 +38,8 @@ module('Integration | Component | page/pki-issuer-details', function (hooks) {
this.context this.context
); );
assert.dom(SELECTORS.rotateRoot).hasText('Rotate this root'); // Add back when rotate root capability is added
// assert.dom(SELECTORS.rotateRoot).hasText('Rotate this root');
assert.dom(SELECTORS.crossSign).hasText('Cross-sign Issuer'); assert.dom(SELECTORS.crossSign).hasText('Cross-sign Issuer');
assert.dom(SELECTORS.signIntermediate).hasText('Sign Intermediate'); assert.dom(SELECTORS.signIntermediate).hasText('Sign Intermediate');
assert.dom(SELECTORS.download).hasText('Download'); assert.dom(SELECTORS.download).hasText('Download');

View File

@ -46,17 +46,15 @@ module('Integration | Component | pki-role-form', function (hooks) {
test('it should save a new pki role with various options selected', async function (assert) { test('it should save a new pki role with various options selected', async function (assert) {
// Key usage, Key params and Not valid after options are tested in their respective component tests // Key usage, Key params and Not valid after options are tested in their respective component tests
assert.expect(10); assert.expect(9);
this.server.post(`/${this.model.backend}/roles/test-role`, (schema, req) => { this.server.post(`/${this.model.backend}/roles/test-role`, (schema, req) => {
assert.ok(true, 'Request made to save role'); assert.ok(true, 'Request made to save role');
const request = JSON.parse(req.requestBody); const request = JSON.parse(req.requestBody);
const roleName = request.name;
const allowedDomainsTemplate = request.allowed_domains_template; const allowedDomainsTemplate = request.allowed_domains_template;
const policyIdentifiers = request.policy_identifiers; const policyIdentifiers = request.policy_identifiers;
const allowedUriSansTemplate = request.allow_uri_sans_template; const allowedUriSansTemplate = request.allow_uri_sans_template;
const allowedSerialNumbers = request.allowed_serial_numbers; const allowedSerialNumbers = request.allowed_serial_numbers;
assert.strictEqual(roleName, 'test-role', 'correctly sends the role name');
assert.true(allowedDomainsTemplate, 'correctly sends allowed_domains_template'); assert.true(allowedDomainsTemplate, 'correctly sends allowed_domains_template');
assert.strictEqual(policyIdentifiers[0], 'some-oid', 'correctly sends policy_identifiers'); assert.strictEqual(policyIdentifiers[0], 'some-oid', 'correctly sends policy_identifiers');
assert.true(allowedUriSansTemplate, 'correctly sends allowed_uri_sans_template'); assert.true(allowedUriSansTemplate, 'correctly sends allowed_uri_sans_template');

View File

@ -0,0 +1,40 @@
import { module, test } from 'qunit';
import { setupTest } from 'vault/tests/helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Unit | Adapter | pki/sign-intermediate', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.backend = 'pki-test';
this.secretMountPath.currentPath = this.backend;
this.payload = {
issuerRef: 'my-issuer-id',
};
});
test('it exists', function (assert) {
const adapter = this.owner.lookup('adapter:pki/sign-intermediate');
assert.ok(adapter);
});
test('it calls the correct endpoint on save', async function (assert) {
assert.expect(2);
this.server.post(`${this.backend}/issuer/my-issuer-id/sign-intermediate`, () => {
assert.ok(true, 'correct endpoint called');
return {
request_id: 'unique-request-id',
data: {
foo: 'bar',
},
};
});
const result = await this.store.createRecord('pki/sign-intermediate', this.payload).save();
assert.strictEqual(result.id, 'unique-request-id', 'Resulting model has ID matching request ID');
});
});

View File

@ -1,6 +1,5 @@
import PkiCertificateBaseModel from './certificate/base'; import PkiCertificateBaseModel from './certificate/base';
import { FormField, FormFieldGroups } from 'vault/app-types'; import { FormField, FormFieldGroups, ModelValidations } from 'vault/app-types';
export default class PkiIssuerModel extends PkiCertificateBaseModel { export default class PkiIssuerModel extends PkiCertificateBaseModel {
useOpenAPI(): boolean; useOpenAPI(): boolean;
issuerId: string; issuerId: string;
@ -18,12 +17,14 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
rotateInternal: any; rotateInternal: any;
rotateExisting: any; rotateExisting: any;
crossSignPath: any; crossSignPath: any;
signIntermediate: any; signIntermediate: any;
-------------------- **/ -------------------- **/
formFields: Array<FormField>; formFields: Array<FormField>;
formFieldGroups: FormFieldGroups; formFieldGroups: FormFieldGroups;
allFields: Array<FormField>;
get canRotateIssuer(): boolean; get canRotateIssuer(): boolean;
get canCrossSign(): boolean; get canCrossSign(): boolean;
get canSignIntermediate(): boolean; get canSignIntermediate(): boolean;
get canConfigure(): boolean; get canConfigure(): boolean;
validate(): ModelValidations;
} }