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 @@ +
+
+ +
+ +
+ +

+ 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. +

+
+
+ + +
+ +
+
\ 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==