Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: PKI Sign Intermediate #18842

Merged
merged 22 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`;
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
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) {
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
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
6 changes: 4 additions & 2 deletions ui/app/models/pki/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
get useOpenAPI() {
return false;
}
@attr('date', {
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
label: 'Issue date',
})
notValidBefore;

@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
95 changes: 95 additions & 0 deletions ui/app/models/pki/sign-intermediate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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. Learn more here.',
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
})
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({
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
subText:
'A comma separated string (or, string array) containing DNS domains for which certificates are allowed to be issued or signed by this CA certificate.',
})
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 }}
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
<li class="action">
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
<DownloadButton
class="toolbar-link"
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
@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="toolbar-link"
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
@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;
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
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}}