Skip to content

Commit

Permalink
UI: PKI Sign Certificate (#18343)
Browse files Browse the repository at this point in the history
  • Loading branch information
hashishaw authored and AnPucel committed Jan 14, 2023
1 parent cbfe20b commit 3ba0971
Show file tree
Hide file tree
Showing 23 changed files with 288 additions and 53 deletions.
13 changes: 13 additions & 0 deletions ui/app/adapters/pki/certificate/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ApplicationAdapter from '../../application';

export default class PkiCertificateBaseAdapter extends ApplicationAdapter {
namespace = 'v1';

deleteRecord(store, type, snapshot) {
const { backend, serialNumber, certificate } = snapshot.record;
// Revoke certificate requires either serial_number or certificate
const data = serialNumber ? { serial_number: serialNumber } : { certificate };
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/revoke`, 'POST', { data });
}
}
19 changes: 5 additions & 14 deletions ui/app/adapters/pki/certificate/generate.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ApplicationAdapter from '../../application';

export default class PkiCertificateGenerateAdapter extends ApplicationAdapter {
namespace = 'v1';

deleteRecord(store, type, snapshot) {
const { backend, serialNumber, certificate } = snapshot.record;
// Revoke certificate requires either serial_number or certificate
const data = serialNumber ? { serial_number: serialNumber } : { certificate };
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/revoke`, 'POST', { data });
}
import PkiCertificateBaseAdapter from './base';

export default class PkiCertificateGenerateAdapter extends PkiCertificateBaseAdapter {
urlForCreateRecord(modelName, snapshot) {
const { name, backend } = snapshot.record;
if (!name || !backend) {
const { role, backend } = snapshot.record;
if (!role || !backend) {
throw new Error('URL for create record is missing required attributes');
}
return `${this.buildURL()}/${encodePath(backend)}/issue/${encodePath(name)}`;
return `${this.buildURL()}/${encodePath(backend)}/issue/${encodePath(role)}`;
}
}
12 changes: 12 additions & 0 deletions ui/app/adapters/pki/certificate/sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { encodePath } from 'vault/utils/path-encoding-helpers';
import PkiCertificateBaseAdapter from './base';

export default class PkiCertificateSignAdapter extends PkiCertificateBaseAdapter {
urlForCreateRecord(modelName, snapshot) {
const { role, backend } = snapshot.record;
if (!role || !backend) {
throw new Error('URL for create record is missing required attributes');
}
return `${this.buildURL()}/${encodePath(backend)}/sign/${encodePath(role)}`;
}
}
4 changes: 2 additions & 2 deletions ui/app/models/pki/certificate/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const generateFromRole = [
default: ['commonName'],
},
{
Options: [
'Subject Alternative Name (SAN) Options': [
'altNames',
'ipSans',
'uriSans',
Expand All @@ -25,5 +25,5 @@ export default class PkiCertificateGenerateModel extends PkiCertificateBaseModel
getHelpUrl(backend) {
return `/v1/${backend}/issue/example?help=1`;
}
@attr('string') name; // associated role
@attr('string') role;
}
45 changes: 45 additions & 0 deletions ui/app/models/pki/certificate/sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';
import PkiCertificateBaseModel from './base';

const generateFromRole = [
{
default: ['csr', 'commonName', 'customTtl', 'format', 'removeRootsFromChain'],
},
{
'Subject Alternative Name (SAN) Options': [
'excludeCnFromSans',
'altNames',
'ipSans',
'uriSans',
'otherSans',
],
},
];
@withFormFields(null, generateFromRole)
export default class PkiCertificateSignModel extends PkiCertificateBaseModel {
getHelpUrl(backend) {
return `/v1/${backend}/sign/example?help=1`;
}
@attr('string') role;

@attr('string', {
label: 'CSR',
editType: 'textarea',
})
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',
})
removeRootsFromChain;
}
5 changes: 3 additions & 2 deletions ui/app/models/pki/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,19 @@ export default class PkiRoleModel extends Model {
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. If no TTL is set, the system uses "default" or the value of max_ttl, whichever is shorter. Alternatively, you can set the not_after date below.',
'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', // The API type is "duration" which accepts both an integer and string e.g. 30 || '30s'
defaultValue: '30s',
})
notBeforeDuration;

Expand Down
10 changes: 3 additions & 7 deletions ui/app/serializers/pki/certificate/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import ApplicationSerializer from '../../application';

export default class PkiCertificateGenerateSerializer extends ApplicationSerializer {
primaryKey = 'serial_number';

serialize() {
const json = super.serialize(...arguments);
// role name is part of the URL, remove from payload
delete json.name;
return json;
}
attrs = {
role: { serialize: false },
};

normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (requestType === 'createRecord' && payload.data.certificate) {
Expand Down
25 changes: 25 additions & 0 deletions ui/app/serializers/pki/certificate/sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { parseCertificate } from 'vault/helpers/parse-pki-cert';
import ApplicationSerializer from '../../application';

export default class PkiCertificateSignSerializer extends ApplicationSerializer {
primaryKey = 'serial_number';
attrs = {
type: { serialize: false },
};

normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (requestType === 'createRecord' && payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
const json = super.normalizeResponse(
store,
primaryModelClass,
{ ...payload, ...parsedCert },
id,
requestType
);
return json;
}
return super.normalizeResponse(...arguments);
}
}
2 changes: 1 addition & 1 deletion ui/lib/core/addon/components/form-field-groups.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{#each @model.fieldGroups as |fieldGroup|}}
{{#each (get @model this.fieldGroups) as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (or (not @renderGroup) (and @renderGroup (eq group @renderGroup)))}}
{{#if (eq group "default")}}
Expand Down
6 changes: 5 additions & 1 deletion ui/lib/core/addon/components/form-field-groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { action } from '@ember/object';
* @param {onChangeCallback} [onChange] - Handler that will get set on the `FormField` component.
* @param {onKeyUpCallback} [onKeyUp] - Handler that will set the value and trigger validation on input changes
* @param {ModelValidations} [modelValidations] - Object containing validation message for each property
*
* @param {string} [groupName='fieldGroups'] - attribute name where the field groups are
*/

export default class FormFieldGroupsComponent extends Component {
Expand All @@ -39,4 +39,8 @@ export default class FormFieldGroupsComponent extends Component {
toggleGroup(group, isOpen) {
this.showGroup = isOpen ? group : null;
}

get fieldGroups() {
return this.args.groupName || 'fieldGroups';
}
}
7 changes: 6 additions & 1 deletion ui/lib/core/addon/components/form-field.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@
</select>
</div>
{{#if this.validationError}}
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
<AlertInline
@type="danger"
@message={{this.validationError}}
@paddingTop={{true}}
data-test-field-validation={{@attr.name}}
/>
{{/if}}
</div>
{{/if}}
Expand Down
8 changes: 7 additions & 1 deletion ui/lib/pki/addon/components/pki-key-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@
</button>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
<AlertInline
@type="danger"
@paddingTop={{true}}
@message={{this.invalidFormAlert}}
@mimicRefresh={{true}}
data-test-pki-key-validation-error
/>
</div>
{{/if}}
</div>
Expand Down
18 changes: 15 additions & 3 deletions ui/lib/pki/addon/components/pki-role-generate.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@
<form {{on "submit" (perform this.save)}} data-test-pki-generate-cert-form>
<div class="box is-bottomless is-fullwidth is-marginless">
<MessageError @errorMessage={{this.errorBanner}} />
<NamespaceReminder @mode={{if @model.isNew "create" "edit"}} @noun="policy" />
<FormFieldGroupsLoop @model={{@model}} @mode="create" @groupName="formFieldGroups" />
<NamespaceReminder @mode="create" @noun="certificate" />
{{#let (get @model.formFieldGroups "0") as |defaultGroup|}}
{{#each defaultGroup.default as |attr|}}
<FormField @model={{@model}} @attr={{attr}}>
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
</FormField>
{{/each}}
{{/let}}
<FormFieldGroups
@model={{@model}}
@mode="create"
@renderGroup="Subject Alternative Name (SAN) Options"
@groupName="formFieldGroups"
/>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
Expand All @@ -60,7 +72,7 @@
disabled={{this.save.isRunning}}
data-test-pki-generate-button
>
Generate
{{capitalize this.verb}}
</button>
<button
type="button"
Expand Down
7 changes: 6 additions & 1 deletion ui/lib/pki/addon/components/pki-role-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import DownloadService from 'vault/services/download';
interface Args {
onSuccess: CallableFunction;
model: PkiCertificateGenerateModel;
type: string;
}

interface PkiCertificateGenerateModel {
Expand Down Expand Up @@ -46,6 +47,10 @@ export default class PkiRoleGenerate extends Component<Args> {
this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.role.details');
}

get verb() {
return this.args.type === 'sign' ? 'sign' : 'generate';
}

@task
*save(evt: Event) {
evt.preventDefault();
Expand All @@ -55,7 +60,7 @@ export default class PkiRoleGenerate extends Component<Args> {
yield model.save();
onSuccess();
} catch (err) {
this.errorBanner = errorMessage(err, 'Could not generate certificate. See Vault logs for details.');
this.errorBanner = errorMessage(err, `Could not ${this.verb} certificate. See Vault logs for details.`);
}
}

Expand Down
12 changes: 12 additions & 0 deletions ui/lib/pki/addon/controllers/roles/role/sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class PkiRolesSignController extends Controller {
@tracked hasSubmitted = false;

@action
toggleTitle() {
this.hasSubmitted = !this.hasSubmitted;
}
}
2 changes: 1 addition & 1 deletion ui/lib/pki/addon/routes/roles/role/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class PkiRoleGenerateRoute extends Route {
async model() {
const { role } = this.paramsFor('roles/role');
return this.store.createRecord('pki/certificate/generate', {
name: role,
role,
});
}

Expand Down
35 changes: 34 additions & 1 deletion ui/lib/pki/addon/routes/roles/role/sign.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class PkiRoleSignRoute extends Route {}
export default class PkiRoleSignRoute extends Route {
@service store;
@service secretMountPath;
@service pathHelp;

beforeModel() {
// Must call this promise before the model hook otherwise
// the model doesn't hydrate from OpenAPI correctly.
return this.pathHelp.getNewModel('pki/certificate/sign', this.secretMountPath.currentPath);
}

model() {
const { role } = this.paramsFor('roles/role');
return this.store.createRecord('pki/certificate/sign', {
role,
});
}

setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const { role } = this.paramsFor('roles/role');
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'roles', route: 'roles.index' },
{ label: role, route: 'roles.role.details' },
{ label: 'sign certificate' },
];
// This is updated on successful generate in the controller
controller.hasSubmitted = false;
}
}
14 changes: 13 additions & 1 deletion ui/lib/pki/addon/templates/roles/role/sign.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
route: roles.role.sign
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-role-page-title>
<Icon @name="certificate" @size="24" class="has-text-grey-light" />
{{if this.hasSubmitted "View signed certificate" "Sign certificate"}}
</h1>
</p.levelLeft>
</PageHeader>

<PkiRoleGenerate @model={{this.model}} @type="sign" @onSuccess={{this.toggleTitle}} />
3 changes: 2 additions & 1 deletion ui/tests/helpers/pki/keys/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export const SELECTORS = {
typeInput: '[data-test-input="type"]',
keyTypeInput: '[data-test-input="keyType"]',
keyBitsInput: '[data-test-input="keyBits"]',
inlineAlert: '[data-test-inline-error-message]',
validationError: '[data-test-pki-key-validation-error]',
fieldErrorByName: (name) => `[data-test-field-validation="${name}"]`,
};
2 changes: 1 addition & 1 deletion ui/tests/helpers/pki/pki-role-generate.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const SELECTORS = {
form: '[data-test-pki-generate-cert-form]',
commonNameField: '[data-test-input="commonName"]',
optionsToggle: '[data-test-toggle-group="Options"]',
optionsToggle: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
generateButton: '[data-test-pki-generate-button]',
cancelButton: '[data-test-pki-generate-cancel]',
downloadButton: '[data-test-pki-cert-download-button]',
Expand Down
Loading

0 comments on commit 3ba0971

Please sign in to comment.