Skip to content

Commit

Permalink
UI: PKI Sign Intermediate (#18842)
Browse files Browse the repository at this point in the history
  • Loading branch information
hashishaw committed Jan 27, 2023
1 parent 4672f94 commit 0b0d269
Show file tree
Hide file tree
Showing 22 changed files with 567 additions and 30 deletions.
3 changes: 3 additions & 0 deletions changelog/18842.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**New PKI UI**: Add beta support for new and improved PKI UI
```
2 changes: 1 addition & 1 deletion ui/app/adapters/pki/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
19 changes: 19 additions & 0 deletions ui/app/adapters/pki/sign-intermediate.js
Original file line number Diff line number Diff line change
@@ -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,
}));
}
}
4 changes: 2 additions & 2 deletions ui/app/models/pki/certificate/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 0 additions & 2 deletions ui/app/models/pki/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
97 changes: 97 additions & 0 deletions ui/app/models/pki/sign-intermediate.js
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 5 additions & 1 deletion ui/app/serializers/pki/role.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import ApplicationSerializer from '../application';

export default class PkiRoleSerializer extends ApplicationSerializer {}
export default class PkiRoleSerializer extends ApplicationSerializer {
attrs = {
name: { serialize: false },
};
}
5 changes: 4 additions & 1 deletion ui/lib/core/addon/components/form-field.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
62 changes: 49 additions & 13 deletions ui/lib/pki/addon/components/page/pki-issuer-details.hbs
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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))}}
Expand Down
2 changes: 2 additions & 0 deletions ui/lib/pki/addon/components/pki-generate-toggle-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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'],
Expand Down
81 changes: 81 additions & 0 deletions ui/lib/pki/addon/components/pki-sign-intermediate-form.hbs
Original file line number Diff line number Diff line change
@@ -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 comments on commit 0b0d269

Please sign in to comment.