Skip to content

Commit

Permalink
UI: pki import issuer (#18634)
Browse files Browse the repository at this point in the history
* create pki ca import component

* add serial number to cert parser

* convert to ts

* remove comments

* reset yarn.lock

* fixed yarn lock

* fix comment

* add declaration for base cert
  • Loading branch information
hellobontempo authored and AnPucel committed Feb 3, 2023
1 parent 4a2fba9 commit 44e5ed5
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 4 deletions.
16 changes: 16 additions & 0 deletions ui/app/adapters/pki/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ export default class PkiIssuerAdapter extends ApplicationAdapter {
}
}

createRecord(store, type, snapshot) {
const { record, adapterOptions } = snapshot;
let url = this.urlForQuery(record.backend);
if (adapterOptions.import) {
url = `${url}/import/bundle`;
} else {
// TODO WIP generate root or intermediate CSR actions from issuers index page
// certType = 'root' || 'intermediate', // record.type is internal or exported
// url = ` ${url}/generate/${certType}/${record.type}`;
throw new Error('createRecord method in adapters/pki/issuer.js is incomplete.');
}
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((resp) => {
return resp;
});
}

query(store, type, query) {
return this.ajax(this.urlForQuery(query.backend), 'GET', this.optionsForQuery());
}
Expand Down
5 changes: 5 additions & 0 deletions ui/app/helpers/parse-pki-cert.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { helper } from '@ember/component/helper';
import * as asn1js from 'asn1js';
import { fromBase64, stringToArrayBuffer } from 'pvutils';
import { Convert } from 'pvtsutils';
import { Certificate } from 'pkijs';

export function parseCertificate(certificateContent) {
Expand Down Expand Up @@ -43,9 +44,13 @@ export function parseCertificate(certificateContent) {
// field themselves are Time values.
const expiryDate = cert?.notAfter?.value;
const issueDate = cert?.notBefore?.value;
const serialNumber = Convert.ToHex(cert.serialNumber.valueBlock.valueHex)
.match(/.{1,2}/g)
.join(':');
return {
can_parse: true,
common_name: commonName,
serial_number: serialNumber,
expiry_date: expiryDate,
issue_date: issueDate,
not_valid_after: expiryDate.valueOf(),
Expand Down
5 changes: 5 additions & 0 deletions ui/app/models/pki/certificate/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export default class PkiCertificateBaseModel extends Model {
@attr('date') notValidAfter;
@attr('date') notValidBefore;

// For importing
@attr('string') pemBundle;
@attr importedIssuers;
@attr importedKeys;

@lazyCapabilities(apiPath`${'backend'}/revoke`, 'backend') revokePath;
get canRevoke() {
return this.revokePath.get('isLoading') || this.revokePath.get('canCreate') !== false;
Expand Down
36 changes: 36 additions & 0 deletions ui/lib/pki/addon/components/pki-ca-certificate-import.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="field">
<div class="form-section">
<label class="title has-padding-top is-5">
Certificate parameters
</label>
<form {{on "submit" (perform this.submitForm)}} data-test-pki-ca-cert-import-form>
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-l">
<TextFile @onChange={{this.onFileUploaded}} @label="PEM Bundle" />
<p class="has-top-margin-m has-bottom-margin-l">
Issuer URLs (Issuing certificates, CRL distribution points, OCSP servers, and delta CRL URLs) can be specified by
editing the individual issuer once it is uploaded to Vault.
</p>
</div>
<div class="has-top-padding-s">
<button
type="submit"
class="button is-primary {{if this.submitForm.isRunning 'is-loading'}}"
disabled={{this.submitForm.isRunning}}
data-test-pki-ca-cert-import
>
Import issuer
</button>
<button
type="button"
class="button has-left-margin-s"
disabled={{this.submitForm.isRunning}}
{{on "click" this.cancel}}
data-test-pki-ca-cert-cancel
>
Cancel
</button>
</div>
</form>
</div>
</div>
60 changes: 60 additions & 0 deletions ui/lib/pki/addon/components/pki-ca-certificate-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import FlashMessageService from 'vault/services/flash-messages';
import { inject as service } from '@ember/service';
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';

/**
* @module PkiCaCertificateImport
* PkiCaCertificateImport components are used to import PKI CA certificates and keys via pem_bundle.
* https://github.com/hashicorp/vault/blob/main/website/content/api-docs/secret/pki.mdx#import-ca-certificates-and-keys
*
* @example
* ```js
* <PkiCaCertificateImport @model={{this.model}} />
* ```
*
* @param {Object} model - certificate model from route
* @callback onCancel - Callback triggered when cancel button is clicked.
* @callback onSubmit - Callback triggered on submit success.
*/

interface Args {
onSave: CallableFunction;
onCancel: CallableFunction;
model: PkiBaseCertificateModel;
}

export default class PkiCaCertificateImport extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;

@tracked errorBanner = '';

@task
@waitFor
*submitForm(event: Event) {
event.preventDefault();
try {
yield this.args.model.save({ adapterOptions: { import: true } });
this.flashMessages.success('Successfully imported certificate.');
this.args.onSave();
} catch (error) {
this.errorBanner = errorMessage(error);
}
}

@action
onFileUploaded({ value }: { value: string }) {
this.args.model.pemBundle = value;
}

@action
cancel() {
this.args.model.unloadRecord();
this.args.onCancel();
}
}
9 changes: 9 additions & 0 deletions ui/lib/pki/addon/routes/issuers/import.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import PkiIssuersIndexRoute from '.';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';

@withConfirmLeave()
export default class PkiIssuersImportRoute extends PkiIssuersIndexRoute {
@service store;

model() {
return this.store.createRecord('pki/issuer');
}

setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.breadcrumbs.push({ label: 'import' });
Expand Down
18 changes: 16 additions & 2 deletions ui/lib/pki/addon/templates/issuers/import.hbs
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
{{! https://github.com/hashicorp/vault/blob/main/website/content/api-docs/secret/pki.mdx#import-ca-certificates-and-keys }}
route: issuers.import POST /pki/issuers/import/bundle POST /pki/issuers/import/cert
<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>
Import a CA
</h1>
</p.levelLeft>
</PageHeader>

<PkiCaCertificateImport
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
/>
3 changes: 2 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@
"highlight.js": "^10.4.1",
"js-yaml": "^3.13.1",
"lodash": "^4.17.13",
"node-notifier": "^8.0.1"
"node-notifier": "^8.0.1",
"pvtsutils": "^1.3.2"
}
}
141 changes: 141 additions & 0 deletions ui/tests/integration/components/pki/pki-issuer-import-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';

module('Integration | Component | pki issuer import', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
setupEngine(hooks, 'pki'); // https://github.com/ember-engines/ember-engines/pull/653

hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/issuer');
this.backend = 'pki-test';
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = this.backend;
this.pemBundle = `
-----BEGIN CERTIFICATE-----
MIIDRTCCAi2gAwIBAgIUdKagCL6TnN5xLkwhPbNY8JEcY0YwDQYJKoZIhvcNAQEL
BQAwGzEZMBcGA1UEAxMQd3d3LnRlc3QtaW50LmNvbTAeFw0yMzAxMDkxOTA1NTBa
Fw0yMzAyMTAxOTA2MjBaMBsxGTAXBgNVBAMTEHd3dy50ZXN0LWludC5jb20wggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfd5o9JfyRAXH+E1vE2U0xjSqs
A/cxDqsDXRHBnNJvzAa+7gPKXCDQZbr6chjxLXpP6Bv2/O+dZHq1fo/f6q9PDDGW
JYIluwbACpe7W1UB7q9xFkZg85yQsNYokGZlwv/AMGpFBxDwVlNGL+4fxvFTv7uF
mIlDzSIPrzByyCrqAFMNNqNwlAerDt/C6DMZae/rTGXIXsTfUpxPy21bzkeA+70I
YCV1ffK8UnAeBYNUJ+v8+XgTQ5KhRyQ+fscUkO3T2s6f3O9Q2sWxswkf2YmZB+V1
cTZ5w6hqiuFdBXz7GRnACi1/gbWbaExQTJRplArFwIHka7dqJh8tYkXDjai3AgMB
AAGjgYAwfjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQU68/xXIgvsleKkuA8clK/6YslB/IwHwYDVR0jBBgwFoAU68/xXIgvsleKkuA8
clK/6YslB/IwGwYDVR0RBBQwEoIQd3d3LnRlc3QtaW50LmNvbTANBgkqhkiG9w0B
AQsFAAOCAQEAWSff0BH3SJv/XqwN/flqc1CVzOios72/IJ+KBBv0AzFCZ8wJPi+c
hH1bw7tqi01Bgh595TctogDFN1b6pjN+jrlIP4N+FF9Moj79Q+jHQMnuJomyPuI7
i07vqUcxgSmvEBBWOWS+/vxe6TfWDg18nyPf127CWQN8IHTo1f/GavX+XmRve6XT
EWoqcQshEk9i87oqCbaT7B40jgjTAd1r4Cc6P4s1fAGPt9e9eqMj13kTyVDNuCoD
FSZYalrlkASpg+c9oDQIh2MikGQINXHv/zIEHOW93siKMWeA4ni6phHtMg/p5eJt
SxnVZsSzj8QLy2uwX1AADR0QUvJzMxptyA==
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAn3eaPSX8kQFx/hNbxNlNMY0qrAP3MQ6rA10RwZzSb8wGvu4D
ylwg0GW6+nIY8S16T+gb9vzvnWR6tX6P3+qvTwwxliWCJbsGwAqXu1tVAe6vcRZG
YPOckLDWKJBmZcL/wDBqRQcQ8FZTRi/uH8bxU7+7hZiJQ80iD68wcsgq6gBTDTaj
cJQHqw7fwugzGWnv60xlyF7E31KcT8ttW85HgPu9CGAldX3yvFJwHgWDVCfr/Pl4
E0OSoUckPn7HFJDt09rOn9zvUNrFsbMJH9mJmQfldXE2ecOoaorhXQV8+xkZwAot
f4G1m2hMUEyUaZQKxcCB5Gu3aiYfLWJFw42otwIDAQABAoIBADC+vZ4Ne4vTtkWl
Izsj9Y29Chs0xx3uzuWjUGcvib/0zOcWGICF8t3hCuu9btRiQ24jlFDGdnRVH5FV
E6OtuFLgdlPgOU1RQzn2wvTZcT26+VQHLBI8xVIRTBVwNmzK06Sq6AEbrNjaenAM
/KwoAuLHzAmFXAgmr0++DIA5oayPWyi5IoyFO7EoRv79Xz5LWfu5j8CKOFXmI5MT
vEVYM6Gb2xHRa2Ng0SJ4VzwC09GcXlHKRAz+CubJuncvjbcM/EryvexozKkUq4XA
KqGr9xxdZ4XDlo3Rj9S9P9JaOin0I1mwwz6p+iwMF0zr+/ldjE4oPBdB1PUgSJ7j
2CZcS1kCgYEAwIZ3UsMIXqkMlkMz/7nu2sqzV3EgQjY5QRoz98ligKg4fhYKz+K4
yXvJrRyLkwEBaPdLppCZbs4xsuuv3jiqUHV5n7sfpUA5HVKkKh6XY7jnszbqV732
iB1mQVEjzM92/amew2hDKLGQDW0nglrg6uV+bx0Lnp6Glahr8NOAyk0CgYEA1Ar3
jTqTkU+NQX7utlxx0HPVL//JH/erp/Gnq9fN8dZhK/yjwX5savUlNHpgePoXf1pE
lgi21/INQsvp7O2AUKuj96k+jBHQ0SS58AQGFv8iNDkLE57N74vCO6+Xdi1rHj/Y
7jglr00box/7SOmvb4SZz2o0jm0Ejsg2M0aBuRMCgYEAgTB6F34qOqMDgD1eQka5
QfXs/Es8E1Ihf08e+jIXuC+poOoXnUINL56ySUizXBS7pnzzNbUoUFNqxB4laF/r
4YvC7m15ocED0mpnIKBghBlK2VaLUA93xAS+XiwdcszwkuzkTUnEbyUfffL2JSHo
dZdEDTmXV3wW4Ywfyn2Sma0CgYAeNNG/FLEg6iw9QE/ROqob/+RGyjFklGunqQ0x
tbRo1xlQotTRI6leMz3xk91aXoYqZjmPBf7GFH0/Hr1cOxkkZM8e4MVAPul4Ybr7
LheP/xhoSBgD24OKtGYfCoyRETdJP98vUGBN8LYXLt8lK+UKBeHDYmXKRE156ZuP
AmRIcQKBgFvp+xMoyAsBeOlTjVDZ0mTnFh1yp8f7N3yXdHPpFShwjXjlqLmLO5RH
mZAvaH0Ux/wCfvwHhdC46jBrs9S4zLBvj3+44NYOzvz2dBWP/5MuXgzFe30h9Yd0
zUlyEaWm0jY2Ylzax8ECKRL0td2bv36vxOYtTax8MSB15szsnPJ+
-----END RSA PRIVATE KEY-----
`;
});

test('it renders import and updates model', async function (assert) {
assert.expect(3);
await render(
hbs`
<PkiCaCertificateImport
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ owner: this.engine }
);

assert.dom('[data-test-pki-ca-cert-import-form]').exists('renders form');
assert.dom('[data-test-component="text-file"]').exists('renders text file input');
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
assert.strictEqual(this.model.pemBundle, this.pemBundle);
});

test('it sends correct payload to import endpoint', async function (assert) {
assert.expect(3);
this.server.post(`/${this.backend}/issuers/import/bundle`, (schema, req) => {
assert.ok(true, 'Request made to the correct endpoint to import issuer');
const request = JSON.parse(req.requestBody);
assert.propEqual(
request,
{
pem_bundle: `${this.pemBundle}`,
},
'sends params in correct type'
);
return {};
});

this.onSave = () => assert.ok(true, 'onSave callback fires on save success');

await render(
hbs`
<PkiCaCertificateImport
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ owner: this.engine }
);

await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
assert.strictEqual(this.model.pemBundle, this.pemBundle);
await click('[data-test-pki-ca-cert-import]');
});

test('it should unload record on cancel', async function (assert) {
assert.expect(2);
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
await render(
hbs`
<PkiCaCertificateImport
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ owner: this.engine }
);

await click('[data-test-pki-ca-cert-cancel]');
assert.true(this.model.isDestroyed, 'new model is unloaded on cancel');
});
});
5 changes: 5 additions & 0 deletions ui/types/generate-declaration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
To generate a declaration file run `yarn tsc <javascript file to declare> --declaration --allowJs --emitDeclarationOnly --outDir <type file location>`

For example, the following command generates a declaration file called base.d.ts for the pki certificate base.js model:

`yarn tsc ./app/models/pki/certificate/base.js --declaration --allowJs --emitDeclarationOnly --outDir types/vault/models/pki/certificate`
22 changes: 22 additions & 0 deletions ui/types/vault/models/pki/certificate/base.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Model from '@ember-data/model';
export default class PkiCertificateBaseModel extends Model {
secretMountPath: class;
get useOpenAPI(): boolean;
get backend(): string;
getHelpUrl(): void;
commonName: string;
caChain: string;
certificate: string;
expiration: number;
issuingCa: string;
privateKey: string;
privateKeyType: string;
serialNumber: string;
notValidAfter: date;
notValidBefore: date;
pemBundle: string;
importedIssuers: string[];
importedKeys: string[];
revokePath: string;
get canRevoke(): boolean;
}
9 changes: 8 additions & 1 deletion ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15994,6 +15994,13 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==

pvtsutils@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==
dependencies:
tslib "^2.4.0"

pvutils@^1.0.17, pvutils@latest:
version "1.0.17"
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf"
Expand Down Expand Up @@ -18218,7 +18225,7 @@ tslib@^2.0.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==

tslib@^2.1.0:
tslib@^2.1.0, tslib@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
Expand Down

0 comments on commit 44e5ed5

Please sign in to comment.