diff --git a/ui/app/adapters/pki/issuer.js b/ui/app/adapters/pki/issuer.js
index 4c0a5b3b6c..bef1bfe895 100644
--- a/ui/app/adapters/pki/issuer.js
+++ b/ui/app/adapters/pki/issuer.js
@@ -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());
}
diff --git a/ui/app/helpers/parse-pki-cert.js b/ui/app/helpers/parse-pki-cert.js
index 087e45dae8..b049e95b6a 100644
--- a/ui/app/helpers/parse-pki-cert.js
+++ b/ui/app/helpers/parse-pki-cert.js
@@ -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) {
@@ -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(),
diff --git a/ui/app/models/pki/certificate/base.js b/ui/app/models/pki/certificate/base.js
index 6c0cb9fb47..517b70cf7e 100644
--- a/ui/app/models/pki/certificate/base.js
+++ b/ui/app/models/pki/certificate/base.js
@@ -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;
diff --git a/ui/lib/pki/addon/components/pki-ca-certificate-import.hbs b/ui/lib/pki/addon/components/pki-ca-certificate-import.hbs
new file mode 100644
index 0000000000..34af4fba8d
--- /dev/null
+++ b/ui/lib/pki/addon/components/pki-ca-certificate-import.hbs
@@ -0,0 +1,36 @@
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/pki-ca-certificate-import.ts b/ui/lib/pki/addon/components/pki-ca-certificate-import.ts
new file mode 100644
index 0000000000..2afeda264c
--- /dev/null
+++ b/ui/lib/pki/addon/components/pki-ca-certificate-import.ts
@@ -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
+ *
+ * ```
+ *
+ * @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 {
+ @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();
+ }
+}
diff --git a/ui/lib/pki/addon/routes/issuers/import.js b/ui/lib/pki/addon/routes/issuers/import.js
index 184af4b8eb..622af1bf97 100644
--- a/ui/lib/pki/addon/routes/issuers/import.js
+++ b/ui/lib/pki/addon/routes/issuers/import.js
@@ -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' });
diff --git a/ui/lib/pki/addon/templates/issuers/import.hbs b/ui/lib/pki/addon/templates/issuers/import.hbs
index 829143eed4..e39affb67f 100644
--- a/ui/lib/pki/addon/templates/issuers/import.hbs
+++ b/ui/lib/pki/addon/templates/issuers/import.hbs
@@ -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
\ No newline at end of file
+
+
+
+
+
+
+ Import a CA
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/package.json b/ui/package.json
index d040aa92cc..252e017b3d 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -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"
}
}
diff --git a/ui/tests/integration/components/pki/pki-issuer-import-test.js b/ui/tests/integration/components/pki/pki-issuer-import-test.js
new file mode 100644
index 0000000000..0e4aa2b909
--- /dev/null
+++ b/ui/tests/integration/components/pki/pki-issuer-import-test.js
@@ -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`
+
+ `,
+ { 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`
+
+ `,
+ { 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`
+
+ `,
+ { owner: this.engine }
+ );
+
+ await click('[data-test-pki-ca-cert-cancel]');
+ assert.true(this.model.isDestroyed, 'new model is unloaded on cancel');
+ });
+});
diff --git a/ui/types/generate-declaration.md b/ui/types/generate-declaration.md
new file mode 100644
index 0000000000..3bdc1993fc
--- /dev/null
+++ b/ui/types/generate-declaration.md
@@ -0,0 +1,5 @@
+To generate a declaration file run `yarn tsc --declaration --allowJs --emitDeclarationOnly --outDir `
+
+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`
diff --git a/ui/types/vault/models/pki/certificate/base.d.ts b/ui/types/vault/models/pki/certificate/base.d.ts
new file mode 100644
index 0000000000..f57e00d062
--- /dev/null
+++ b/ui/types/vault/models/pki/certificate/base.d.ts
@@ -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;
+}
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 57861f9ddd..108ec9a346 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -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"
@@ -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==