UI: Pki model attribute consolidation (#19281)

This commit is contained in:
claire bontempo 2023-02-24 07:56:12 -08:00 committed by GitHub
parent d8348490d5
commit 16baa1090f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 248 additions and 145 deletions

View File

@ -52,7 +52,7 @@ export default class PkiActionModel extends Model {
label: 'Subject Alternative Names (SANs)',
editType: 'stringArray',
})
altNames; // comma sep strings
altNames;
@attr('string', {
label: 'IP Subject Alternative Names (IP SANs)',
@ -129,7 +129,8 @@ export default class PkiActionModel extends Model {
@attr({ editType: 'stringArray' }) postalCode;
@attr('string', {
subText: "Specifies the requested Subject's named Serial Number value.",
subText:
"Specifies the requested Subject's named Serial Number value. This has no impact on the Certificate's serial number randomly generated by Vault.",
})
serialNumber;

View File

@ -5,10 +5,12 @@ import { withFormFields } from 'vault/decorators/model-form-fields';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
/**
* There are many ways to generate a cert, but we want to display them in a consistent way.
* This base certificate model will set the attributes we want to display, and other
* models under pki/certificate will extend this model and have their own required
* attributes and adapter methods.
* There are many actions that involve certificates in PKI world.
* The base certificate model contains shared attributes that make up a certificate's content.
* Other models under pki/certificate will extend this model and include additional attributes
* and associated adapter methods for performing various generation and signing actions.
* This model also displays leaf certs and their parsed attributes (parsed parameters only
* render if included in certDisplayFields below).
*/
const certDisplayFields = [
@ -23,6 +25,7 @@ const certDisplayFields = [
@withFormFields(certDisplayFields)
export default class PkiCertificateBaseModel extends Model {
@service secretMountPath;
get useOpenAPI() {
return true;
}
@ -33,7 +36,6 @@ export default class PkiCertificateBaseModel extends Model {
assert('You must provide a helpUrl for OpenAPI', true);
}
// Required input for all certificates
@attr('string') commonName;
@attr({
@ -43,33 +45,59 @@ export default class PkiCertificateBaseModel extends Model {
'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; // combines ttl and notAfter into one input <PkiNotValidAfterForm>
customTtl; // sets ttl and notAfter via one input <PkiNotValidAfterForm>
@attr('boolean', {
label: 'Exclude common name from SANs',
subText:
'If checked, the common name will not be included in DNS or Email Subject Alternate Names. This is useful if the CN is a human-readable identifier, not a hostname or email address.',
defaultValue: false,
})
excludeCnFromSans;
@attr('string', {
label: 'Subject Alternative Names (SANs)',
subText:
'The requested Subject Alternative Names; if email protection is enabled for the role, this may contain email addresses. Add one per row.',
editType: 'stringArray',
})
altNames;
// SANs below are editType: stringArray from openApi
@attr('string', {
label: 'IP Subject Alternative Names (IP SANs)',
subText: 'Only valid if the role allows IP SANs (which is the default). Add one per row.',
})
ipSans;
@attr('string', {
label: 'URI Subject Alternative Names (URI SANs)',
subText:
'If any requested URIs do not match role policy, the entire request will be denied. Add one per row.',
})
uriSans;
@attr('string', {
subText:
'Requested other SANs with the format <oid>;UTF8:<utf8 string value> for each entry. Add one per row.',
})
otherSans;
// Attrs that come back from API POST request
@attr({ masked: true, label: 'CA Chain' }) caChain;
@attr({ label: 'CA Chain', masked: true }) caChain;
@attr('string', { masked: true }) certificate;
@attr('number') expiration;
@attr('number', { formatDate: true }) revocationTime;
@attr('string', { label: 'Issuing CA', masked: true }) issuingCa;
@attr('string') privateKey;
@attr('string') privateKeyType;
@attr('string') privateKey; // only returned for type=exported
@attr('string') privateKeyType; // only returned for type=exported
@attr('number', { formatDate: true }) revocationTime;
@attr('string') serialNumber;
// Parsed from cert in serializer
@attr('number', { formatDate: true }) notValidAfter;
@attr('number', { formatDate: true }) notValidBefore;
@attr('string', { label: 'URI Subject Alternative Names (URI SANs)' }) uriSans;
@attr('string', { label: 'IP Subject Alternative Names (IP SANs)' }) ipSans;
@attr('string', { label: 'Subject Alternative Names (SANs)' }) altNames;
// read only attrs parsed from certificate contents in serializer on GET requests (see parse-pki-cert.js)
@attr('number', { formatDate: true }) notValidAfter; // set by ttl or notAfter (customTtL above)
@attr('number', { formatDate: true }) notValidBefore; // date certificate was issued
@attr('string') signatureBits;
// For importing
@attr('string') pemBundle;
// readonly attrs returned after importing
@attr importedIssuers;
@attr importedKeys;
@attr mapping;
@lazyCapabilities(apiPath`${'backend'}/revoke`, 'backend') revokePath;
get canRevoke() {
return this.revokePath.get('isLoading') || this.revokePath.get('canCreate') !== false;

View File

@ -8,11 +8,11 @@ const generateFromRole = [
},
{
'Subject Alternative Name (SAN) Options': [
'excludeCnFromSans',
'altNames',
'ipSans',
'uriSans',
'otherSans',
'excludeCnFromSans',
],
},
{
@ -24,5 +24,5 @@ export default class PkiCertificateGenerateModel extends PkiCertificateBaseModel
getHelpUrl(backend) {
return `/v1/${backend}/issue/example?help=1`;
}
@attr('string') role;
@attr('string') role; // role name to issue certificate against for request URL
}

View File

@ -21,7 +21,7 @@ export default class PkiCertificateSignModel extends PkiCertificateBaseModel {
getHelpUrl(backend) {
return `/v1/${backend}/sign/example?help=1`;
}
@attr('string') role;
@attr('string') role; // role name to create certificate against for request URL
@attr('string', {
label: 'CSR',
@ -29,17 +29,8 @@ export default class PkiCertificateSignModel extends PkiCertificateBaseModel {
})
csr;
@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('boolean', {
subText: 'When checked, the CA chain will not include self-signed CA certificates',
subText: 'When checked, the CA chain will not include self-signed CA certificates.',
})
removeRootsFromChain;
}

View File

@ -1,49 +1,69 @@
import PkiCertificateBaseModel from './certificate/base';
import { attr } from '@ember-data/model';
import Model, { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { service } from '@ember/service';
const issuerUrls = ['issuingCertificates', 'crlDistributionPoints', 'ocspServers'];
@withFormFields(
['issuerName', 'leafNotAfterBehavior', 'usage', 'manualChain', ...issuerUrls],
[
{
default: [
'certificate',
'caChain',
'commonName',
'issuerName',
'issuerId',
'serialNumber',
'keyId',
'uriSans',
'ipSans',
'notValidBefore',
'notValidAfter',
],
},
{ 'Issuer URLs': issuerUrls },
]
)
export default class PkiIssuerModel extends PkiCertificateBaseModel {
// there are too many differences between what openAPI returns and the designs for the update form
// manually defining the attrs instead with the correct meta data
const inputFields = [
'issuerName',
'leafNotAfterBehavior',
'usage',
'manualChain',
'revocationSignatureAlgorithm',
...issuerUrls,
];
const displayFields = [
{
default: [
'certificate',
'caChain',
'commonName',
'issuerName',
'issuerId',
'serialNumber',
'keyId',
'altNames',
'uriSans',
'ipSans',
'otherSans',
'notValidBefore',
'notValidAfter',
],
},
{ 'Issuer URLs': issuerUrls },
];
@withFormFields(inputFields, displayFields)
export default class PkiIssuerModel extends Model {
@service secretMountPath;
// TODO use openAPI after removing route extension (see pki/roles route for example)
get useOpenAPI() {
return false;
}
get backend() {
return this.secretMountPath.currentPath;
}
get issuerRef() {
return this.issuerName || this.issuerId;
}
@attr isDefault; // readonly
@attr('string') issuerId;
// READ ONLY
@attr isDefault;
@attr('string', { label: 'Issuer ID' }) issuerId;
@attr('string', { label: 'Default key ID' }) keyId;
@attr({ label: 'CA Chain', masked: true }) caChain;
@attr({ masked: true }) certificate;
@attr('string', {
label: 'Default key ID',
})
keyId;
// parsed from certificate contents in serializer (see parse-pki-cert.js)
@attr commonName;
@attr('number', { formatDate: true }) notValidAfter;
@attr('number', { formatDate: true }) notValidBefore;
@attr serialNumber; // this is not the UUID serial number field randomly generated by Vault for leaf certificates
@attr({ label: 'Subject Alternative Names (SANs)' }) altNames;
@attr({ label: 'IP SANs' }) ipSans;
@attr({ label: 'URI SANs' }) uriSans;
@attr({ label: 'Other SANs' }) otherSans;
// UPDATING
@attr('string') issuerName;
@attr({
@ -57,7 +77,6 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
leafNotAfterBehavior;
@attr({
label: 'Usage',
subText: 'Allowed usages for this issuer. It can always be read.',
editType: 'yield',
valueOptions: [
@ -69,14 +88,32 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
usage;
@attr('string', {
label: 'Manual chain',
subText:
"An advanced field useful when automatic chain building isn't desired. The first element must be the present issuer's reference.",
})
manualChain;
@attr({
subText:
'The signature algorithm to use when building CRLs. The default value (empty string) is for Go to select the signature algorithm automatically, which may not always work.',
noDefault: true,
possibleValues: [
'sha256withrsa',
'ecdsawithsha384',
'sha256withrsapss',
'ed25519',
'sha384withrsapss',
'sha512withrsapss',
'pureed25519',
'sha384withrsa',
'sha512withrsa',
'ecdsawithsha256',
'ecdsawithsha512',
],
})
revocationSignatureAlgorithm;
@attr('string', {
label: 'Issuing certificates',
subText:
'The URL values for the Issuing Certificate field. These are different URLs for the same resource, and should be added individually, not in a comma-separated list.',
editType: 'stringArray',
@ -97,6 +134,13 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
})
ocspServers;
// IMPORTING
@attr('string') pemBundle;
// readonly attrs returned after importing
@attr importedIssuers;
@attr importedKeys;
@attr mapping;
@lazyCapabilities(apiPath`${'backend'}/issuer/${'issuerId'}`) issuerPath;
@lazyCapabilities(apiPath`${'backend'}/root/rotate/exported`) rotateExported;
@lazyCapabilities(apiPath`${'backend'}/root/rotate/internal`) rotateInternal;

View File

@ -11,6 +11,7 @@ const validations = {
'csr',
'useCsrValues',
'commonName',
'excludeCnFromSans',
'customTtl',
'notBeforeDuration',
'format',
@ -39,15 +40,6 @@ export default class PkiSignIntermediateModel extends PkiCertificateBaseModel {
})
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',
@ -59,9 +51,6 @@ export default class PkiSignIntermediateModel extends PkiCertificateBaseModel {
})
notBeforeDuration;
@attr('string')
commonName;
@attr({
label: 'Permitted DNS domains',
subText:
@ -94,4 +83,11 @@ export default class PkiSignIntermediateModel extends PkiCertificateBaseModel {
possibleValues: ['0', '256', '384', '512'],
})
signatureBits;
/* Additional subject overrides */
@attr('string', {
subText:
"Specifies the requested Subject's named Serial Number value. This has no impact on the Certificate's serial number randomly generated by Vault.",
})
serialNumber;
}

View File

@ -8,15 +8,19 @@ export default class PkiIssuerSerializer extends ApplicationSerializer {
super(...arguments);
// remove following attrs from serialization
const attrs = [
'altNames',
'caChain',
'certificate',
'commonName',
'ipSans',
'issuerId',
'keyId',
'otherSans',
'notValidAfter',
'notValidBefore',
'serialNumber',
'signatureBits',
'uriSans',
];
this.attrs = attrs.reduce((attrObj, attr) => {
attrObj[attr] = { serialize: false };

View File

@ -13,6 +13,7 @@
@onChange={{@onChange}}
@onKeyUp={{@onKeyUp}}
@modelValidations={{@modelValidations}}
@showHelpText={{@showHelpText}}
/>
{{else}}
{{! OTHERWISE WE'RE EDITING }}
@ -27,6 +28,7 @@
@onChange={{@onChange}}
@onKeyUp={{@onKeyUp}}
@modelValidations={{@modelValidations}}
@showHelpText={{@showHelpText}}
/>
{{/if}}
{{/if}}
@ -53,6 +55,7 @@
@onChange={{@onChange}}
@onKeyUp={{@onKeyUp}}
@modelValidations={{@modelValidations}}
@showHelpText={{@showHelpText}}
/>
{{else}}
{{! OTHERWISE WE'RE EDITING }}
@ -67,6 +70,7 @@
@onChange={{@onChange}}
@onKeyUp={{@onKeyUp}}
@modelValidations={{@modelValidations}}
@showHelpText={{@showHelpText}}
/>
{{/if}}
{{/if}}

View File

@ -6,7 +6,7 @@ import { task } from 'ember-concurrency';
import { tracked } from '@glimmer/tracking';
import { waitFor } from '@ember/test-waiters';
import errorMessage from 'vault/utils/error-message';
import PkiBaseCertificateModel from 'vault/models/pki/certificate/base';
import PkiIssuerModel from 'vault/models/pki/issuer';
import PkiActionModel from 'vault/models/pki/action';
/**
@ -27,7 +27,7 @@ import PkiActionModel from 'vault/models/pki/action';
interface Args {
onSave: CallableFunction;
onCancel: CallableFunction;
model: PkiBaseCertificateModel | PkiActionModel;
model: PkiIssuerModel | PkiActionModel;
adapterOptions: object | undefined;
}

View File

@ -1,32 +1,34 @@
<div class="box is-bottomless is-fullwidth is-marginless">
<div class="columns">
{{#each this.configTypes as |option|}}
<div class="column is-flex">
<label for={{option.key}} class="box-label is-column {{if (eq @config.actionType option.key) 'is-selected'}}">
<div>
<h3 class="box-label-header title is-6">
<Icon @size="24" @name={{option.icon}} />
{{option.label}}
</h3>
<p class="help has-text-grey-dark">
{{option.description}}
</p>
</div>
<div>
<RadioButton
id={{option.key}}
name="pki-config-type"
@value={{option.key}}
@groupValue={{@config.actionType}}
@onChange={{fn (mut @config.actionType) option.key}}
data-test-pki-config-option={{option.key}}
/>
<label for={{option.key}}></label>
</div>
</label>
</div>
{{/each}}
</div>
{{#unless @config.id}}
<div class="columns">
{{#each this.configTypes as |option|}}
<div class="column is-flex">
<label for={{option.key}} class="box-label is-column {{if (eq @config.actionType option.key) 'is-selected'}}">
<div>
<h3 class="box-label-header title is-6">
<Icon @size="24" @name={{option.icon}} />
{{option.label}}
</h3>
<p class="help has-text-grey-dark">
{{option.description}}
</p>
</div>
<div>
<RadioButton
id={{option.key}}
name="pki-config-type"
@value={{option.key}}
@groupValue={{@config.actionType}}
@onChange={{fn (mut @config.actionType) option.key}}
data-test-pki-config-option={{option.key}}
/>
<label for={{option.key}}></label>
</div>
</label>
</div>
{{/each}}
</div>
{{/unless}}
{{#if (eq @config.actionType "import")}}
<PkiCaCertificateImport
@model={{@config}}

View File

@ -39,10 +39,11 @@
{{else if (eq group "Additional subject fields")}}
These fields provide more information about the client to which the certificate belongs.
{{/if}}
Add one item per row.
</p>
{{#each fields as |fieldName|}}
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
<FormField data-test-field @attr={{attr}} @mode="create" @model={{@model}} />
<FormField data-test-field @attr={{attr}} @model={{@model}} @showHelpText={{false}} />
{{/let}}
{{/each}}
{{/if}}

View File

@ -14,11 +14,11 @@
{{/let}}
<FormFieldGroups
@model={{@model}}
@mode="create"
@renderGroup="Subject Alternative Name (SAN) Options"
@groupName="formFieldGroups"
@showHelpText={{false}}
/>
<FormFieldGroups @model={{@model}} @mode="create" @renderGroup="More Options" @groupName="formFieldGroups" />
<FormFieldGroups @model={{@model}} @renderGroup="More Options" @groupName="formFieldGroups" />
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">

View File

@ -4,13 +4,13 @@ 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 PkiCertificateSignIntermediate from 'vault/models/pki/certificate/sign-intermediate';
import FlashMessageService from 'vault/services/flash-messages';
import errorMessage from 'vault/utils/error-message';
interface Args {
onCancel: CallableFunction;
model: PkiIssuerModel;
model: PkiCertificateSignIntermediate;
}
export default class PkiSignIntermediateFormComponent extends Component<Args> {
@ -52,6 +52,7 @@ export default class PkiSignIntermediateFormComponent extends Component<Args> {
'province',
'streetAddress',
'postalCode',
'serialNumber', // this is different from the UUID serial number generated by vault (in show fields below)
],
};
}

View File

@ -429,10 +429,10 @@ module('Acceptance | pki workflow', function (hooks) {
assert.dom(SELECTORS.issuerDetails.title).hasText('View issuer certificate');
assert
.dom(`${SELECTORS.issuerDetails.defaultGroup} ${SELECTORS.issuerDetails.row}`)
.exists({ count: 11 }, 'Renders 10 info table items under default group');
.exists({ count: 13 }, 'Renders 13 info table items under default group');
assert
.dom(`${SELECTORS.issuerDetails.urlsGroup} ${SELECTORS.issuerDetails.row}`)
.exists({ count: 3 }, 'Renders 4 info table items under URLs group');
.exists({ count: 3 }, 'Renders 3 info table items under URLs group');
assert.dom(SELECTORS.issuerDetails.groupTitle).exists({ count: 1 }, 'only 1 group title rendered');
});
});

View File

@ -48,7 +48,9 @@ module('Integration | Component | pki-generate-root', function (hooks) {
await click(SELECTORS.additionalGroupToggle);
assert
.dom(SELECTORS.toggleGroupDescription)
.hasText('These fields provide more information about the client to which the certificate belongs.');
.hasText(
'These fields provide more information about the client to which the certificate belongs. Add one item per row.'
);
assert
.dom(`[data-test-group="Additional subject fields"] ${SELECTORS.formField}`)
.exists({ count: 7 }, '7 form fields under Additional Fields toggle');
@ -57,7 +59,7 @@ module('Integration | Component | pki-generate-root', function (hooks) {
assert
.dom(SELECTORS.toggleGroupDescription)
.hasText(
'SAN fields are an extension that allow you specify additional host names (sites, IP addresses, common names, etc.) to be protected by a single certificate.'
'SAN fields are an extension that allow you specify additional host names (sites, IP addresses, common names, etc.) to be protected by a single certificate. Add one item per row.'
);
assert
.dom(`[data-test-group="Subject Alternative Name (SAN) Options"] ${SELECTORS.formField}`)

View File

@ -42,7 +42,7 @@ module('Integration | Component | pki-sign-intermediate-form', function (hooks)
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('[data-test-field]').exists({ count: 9 }, '9 default fields shown');
assert.dom(selectors.toggleSigningOptions).exists();
assert.dom(selectors.toggleSANOptions).exists();
assert.dom(selectors.toggleAdditionalFields).exists();

View File

@ -4,19 +4,21 @@ export default class PkiCertificateBaseModel extends Model {
get useOpenAPI(): boolean;
get backend(): string;
getHelpUrl(): void;
altNames: string;
commonName: string;
caChain: string;
certificate: string;
excludeCnFromSans: boolean;
expiration: number;
ipSans: string;
issuingCa: string;
privateKey: string;
privateKeyType: string;
serialNumber: string;
notValidAfter: date;
notValidBefore: date;
pemBundle: string;
importedIssuers: string[];
importedKeys: string[];
otherSans: string;
privateKey: string;
privateKeyType: string;
revokePath: string;
revocationTime: number;
serialNumber: string;
get canRevoke(): boolean;
}

View File

@ -1,10 +1,8 @@
import { FormField } from 'vault/app-types';
import { FormField, FormFieldGroups } from 'vault/app-types';
import PkiCertificateBaseModel from './base';
export default class PkiCertificateGenerateModel extends PkiCertificateBaseModel {
name: string;
role: string;
formFields: FormField[];
formFieldsGroup: {
[k: string]: FormField[];
}[];
formFieldGroups: FormFieldGroups;
}

View File

@ -0,0 +1,17 @@
import PkiCertificateBaseModel from './base';
import { FormField, FormFieldGroups, ModelValidations } from 'vault/app-types';
export default class PkiCertificateSignIntermediateModel extends PkiCertificateBaseModel {
role: string;
csr: string;
formFields: FormField[];
formFieldGroups: FormFieldGroups;
issuerRef: string;
maxPathLength: string;
notBeforeDuration: string;
permittedDnsDomains: string;
useCsrValues: boolean;
usePss: boolean;
skid: string;
signatureBits: string;
validate(): ModelValidations;
}

View File

@ -1,3 +1,10 @@
import PkiCertificateBaseModel from './base';
export default class PkiCertificateGenerateModel extends PkiCertificateBaseModel {}
import { FormField, FormFieldGroups, ModelValidations } from 'vault/app-types';
export default class PkiCertificateSignModel extends PkiCertificateBaseModel {
role: string;
csr: string;
formFields: FormField[];
formFieldGroups: FormFieldGroups;
removeRootsFromChain: boolean;
validate(): ModelValidations;
}

View File

@ -1,10 +1,12 @@
import PkiCertificateBaseModel from './certificate/base';
import Model from '@ember-data/model';
import { FormField, FormFieldGroups, ModelValidations } from 'vault/app-types';
export default class PkiIssuerModel extends PkiCertificateBaseModel {
useOpenAPI(): boolean;
export default class PkiIssuerModel extends Model {
secretMountPath: class;
get useOpenAPI(): boolean;
get backend(): string;
get issuerRef(): string;
issuerId: string;
issuerName: string;
issuerRef(): string;
keyId: string;
uriSans: string;
leafNotAfterBehavior: string;
@ -20,9 +22,12 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
crossSignPath: any;
signIntermediate: any;
-------------------- **/
formFields: Array<FormField>;
pemBundle: string;
importedIssuers: string[];
importedKeys: string[];
formFields: FormField[];
formFieldGroups: FormFieldGroups;
allFields: Array<FormField>;
allFields: FormField[];
get canRotateIssuer(): boolean;
get canCrossSign(): boolean;
get canSignIntermediate(): boolean;