UI: PKI Sign Intermediate (#18842)
This commit is contained in:
parent
419a92a632
commit
8788317b8a
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
**New PKI UI**: Add beta support for new and improved PKI UI
|
||||
```
|
|
@ -24,7 +24,7 @@ export default class PkiActionAdapter extends ApplicationAdapter {
|
|||
? `${baseUrl}/issuers/generate/intermediate/${type}`
|
||||
: `${baseUrl}/intermediate/generate/${type}`;
|
||||
case 'sign-intermediate':
|
||||
return `${baseUrl}/issuer/${issuerName}/sign-intermediate`;
|
||||
return `${baseUrl}/issuer/${encodePath(issuerName)}/sign-intermediate`;
|
||||
default:
|
||||
assert('actionType must be one of import, generate-root, generate-csr or sign-intermediate');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -37,11 +37,11 @@ export default class PkiCertificateBaseModel extends Model {
|
|||
@attr('string') commonName;
|
||||
|
||||
// Attrs that come back from API POST request
|
||||
@attr() caChain;
|
||||
@attr({ masked: true, label: 'CA Chain' }) caChain;
|
||||
@attr('string', { masked: true }) certificate;
|
||||
@attr('number') expiration;
|
||||
@attr('number', { formatDate: true }) revocationTime;
|
||||
@attr('string') issuingCa;
|
||||
@attr('string', { label: 'Issuing CA', masked: true }) issuingCa;
|
||||
@attr('string') privateKey;
|
||||
@attr('string') privateKeyType;
|
||||
@attr('string') serialNumber;
|
||||
|
|
|
@ -32,8 +32,6 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
|
|||
|
||||
@attr isDefault; // readonly
|
||||
@attr('string') issuerId;
|
||||
@attr('string', { displayType: 'masked' }) certificate;
|
||||
@attr('string', { displayType: 'masked', label: 'CA Chain' }) caChain;
|
||||
|
||||
@attr('string', {
|
||||
label: 'Default key ID',
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class PkiRoleSerializer extends ApplicationSerializer {}
|
||||
export default class PkiRoleSerializer extends ApplicationSerializer {
|
||||
attrs = {
|
||||
name: { serialize: false },
|
||||
};
|
||||
}
|
||||
|
|
|
@ -237,8 +237,11 @@
|
|||
id={{@attr.name}}
|
||||
value={{or (get @model this.valuePath) @attr.options.defaultValue}}
|
||||
oninput={{this.onChangeWithEvent}}
|
||||
class="textarea"
|
||||
class="textarea {{if this.validationError 'has-error-border'}}"
|
||||
></textarea>
|
||||
{{#if this.validationError}}
|
||||
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
|
||||
{{/if}}
|
||||
{{else if (eq @attr.options.editType "password")}}
|
||||
<Input
|
||||
data-test-input={{@attr.name}}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @canRotate}}
|
||||
{{!-- {{#if @canRotate}}
|
||||
<ToolbarLink @route="issuers.generate-root" @type="rotate-cw" @issuer={{@issuer.id}} data-test-pki-issuer-rotate-root>
|
||||
Rotate this root
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
{{/if}} --}}
|
||||
{{#if @canCrossSign}}
|
||||
<ToolbarLink
|
||||
@route="issuers.issuer.cross-sign"
|
||||
|
@ -20,16 +20,52 @@
|
|||
Sign Intermediate
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
<DownloadButton
|
||||
class="toolbar-link"
|
||||
@filename={{@issuer.id}}
|
||||
@data={{@issuer.certificate}}
|
||||
@extension="pem"
|
||||
data-test-issuer-download
|
||||
>
|
||||
Download
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</DownloadButton>
|
||||
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||
<D.Trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
data-test-issuer-download
|
||||
>
|
||||
Download
|
||||
<Chevron @direction="down" @isButton={{true}} />
|
||||
</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}}
|
||||
<ToolbarLink @route="issuers.issuer.edit" @issuer={{@issuer.id}} data-test-pki-issuer-configure>
|
||||
Configure
|
||||
|
@ -58,7 +94,7 @@
|
|||
</h2>
|
||||
{{/if}}
|
||||
{{#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}}>
|
||||
<MaskedInput
|
||||
@name={{or attr.options.label (humanize (dasherize attr.name))}}
|
||||
|
|
|
@ -6,6 +6,7 @@ import PkiActionModel from 'vault/models/pki/action';
|
|||
|
||||
interface Args {
|
||||
model: PkiActionModel;
|
||||
groups: Map<[key: string], Array<string>> | null;
|
||||
}
|
||||
|
||||
export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
|
||||
|
@ -21,6 +22,7 @@ export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
|
|||
}
|
||||
|
||||
get groups() {
|
||||
if (this.args.groups) return this.args.groups;
|
||||
const groups = {
|
||||
'Key parameters': this.keyParamFields,
|
||||
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],
|
||||
|
|
|
@ -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}}
|
|
@ -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'];
|
||||
}
|
||||
}
|
|
@ -2,8 +2,30 @@ import PkiIssuerIndexRoute from './index';
|
|||
|
||||
export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute {
|
||||
// Details route gets issuer data from PkiIssuerIndexRoute
|
||||
setupController(controller, resolvedModel) {
|
||||
async setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
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' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,11 @@
|
|||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<Page::PkiIssuerDetails
|
||||
@issuer={{this.model}}
|
||||
@pem={{this.pem}}
|
||||
@der={{this.der}}
|
||||
@canRotate={{this.model.canRotateIssuer}}
|
||||
@canCrossSign={{this.model.canCrossSign}}
|
||||
@canSignIntermediate={{this.model.canSignIntermediate}}
|
||||
|
|
|
@ -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}}
|
||||
/>
|
|
@ -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`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@ const selectors = {
|
|||
keys: '[data-test-toggle-group="Key parameters"]',
|
||||
sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
|
||||
subjectFields: '[data-test-toggle-group="Additional subject fields"]',
|
||||
toggleByName: (name) => `[data-test-toggle-group="${name}"]`,
|
||||
};
|
||||
|
||||
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`);
|
||||
});
|
||||
});
|
||||
|
||||
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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,7 +38,8 @@ module('Integration | Component | page/pki-issuer-details', function (hooks) {
|
|||
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.signIntermediate).hasText('Sign Intermediate');
|
||||
assert.dom(SELECTORS.download).hasText('Download');
|
||||
|
|
|
@ -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) {
|
||||
// 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) => {
|
||||
assert.ok(true, 'Request made to save role');
|
||||
const request = JSON.parse(req.requestBody);
|
||||
const roleName = request.name;
|
||||
const allowedDomainsTemplate = request.allowed_domains_template;
|
||||
const policyIdentifiers = request.policy_identifiers;
|
||||
const allowedUriSansTemplate = request.allow_uri_sans_template;
|
||||
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.strictEqual(policyIdentifiers[0], 'some-oid', 'correctly sends policy_identifiers');
|
||||
assert.true(allowedUriSansTemplate, 'correctly sends allowed_uri_sans_template');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
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 {
|
||||
useOpenAPI(): boolean;
|
||||
issuerId: string;
|
||||
|
@ -18,12 +17,14 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
|
|||
rotateInternal: any;
|
||||
rotateExisting: any;
|
||||
crossSignPath: any;
|
||||
signIntermediate: any;
|
||||
signIntermediate: any;
|
||||
-------------------- **/
|
||||
formFields: Array<FormField>;
|
||||
formFieldGroups: FormFieldGroups;
|
||||
allFields: Array<FormField>;
|
||||
get canRotateIssuer(): boolean;
|
||||
get canCrossSign(): boolean;
|
||||
get canSignIntermediate(): boolean;
|
||||
get canConfigure(): boolean;
|
||||
validate(): ModelValidations;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue