diff --git a/ui/app/adapters/pki/config/import.js b/ui/app/adapters/pki/config/import.js new file mode 100644 index 000000000..0ea601084 --- /dev/null +++ b/ui/app/adapters/pki/config/import.js @@ -0,0 +1,14 @@ +import { encodePath } from 'vault/utils/path-encoding-helpers'; +import ApplicationAdapter from '../../application'; + +export default class PkiConfigImportAdapter extends ApplicationAdapter { + namespace = 'v1'; + + urlForCreateRecord(modelName, snapshot) { + const { backend } = snapshot.record; + if (!backend) { + throw new Error('URL for create record is missing required attributes'); + } + return `${this.buildURL()}/${encodePath(backend)}/issuers/import/bundle`; + } +} diff --git a/ui/app/models/pki/config/import.js b/ui/app/models/pki/config/import.js new file mode 100644 index 000000000..42e6b7ce8 --- /dev/null +++ b/ui/app/models/pki/config/import.js @@ -0,0 +1,14 @@ +import Model, { attr } from '@ember-data/model'; +import { inject as service } from '@ember/service'; + +export default class PkiConfigImportModel extends Model { + @service secretMountPath; + + @attr('string') pemBundle; + @attr importedIssuers; + @attr importedKeys; + + get backend() { + return this.secretMountPath.currentPath; + } +} diff --git a/ui/lib/pki/addon/components/page/pki-issuer-details.hbs b/ui/lib/pki/addon/components/page/pki-issuer-details.hbs index 0b5505a5d..b18f72c8c 100644 --- a/ui/lib/pki/addon/components/page/pki-issuer-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-issuer-details.hbs @@ -59,7 +59,11 @@ {{else if (eq attr.name "keyId")}} - {{get @issuer attr.name}} + {{#if @issuer.keyId}} + {{@issuer.keyId}} + {{else}} + + {{/if}} {{else}} + + {{yield}} +
+
+ + + Cancel + +
+
+ \ No newline at end of file diff --git a/ui/lib/pki/addon/components/pki-config/import.ts b/ui/lib/pki/addon/components/pki-config/import.ts new file mode 100644 index 000000000..16bd6c2c1 --- /dev/null +++ b/ui/lib/pki/addon/components/pki-config/import.ts @@ -0,0 +1,63 @@ +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +// Types +import { HTMLElementEvent } from 'forms'; +import PkiConfigImportModel from 'vault/models/pki/config/import'; +import Store from '@ember-data/store'; +import Router from '@ember/routing/router'; +import FlashMessageService from 'vault/services/flash-messages'; +import errorMessage from 'vault/utils/error-message'; + +interface File { + value: string; + fileName?: string; + enterAsText: boolean; +} + +/** + * Pki Config Import Component creates a PKI Config record on mount, and cleans it up if dirty on unmount + */ +export default class PkiConfigImportComponent extends Component> { + @service declare readonly store: Store; + @service declare readonly router: Router; + @service declare readonly flashMessages: FlashMessageService; + + @tracked configModel: PkiConfigImportModel | null = null; + + constructor(owner: unknown, args: Record) { + super(owner, args); + const model = this.store.createRecord('pki/config/import'); + this.configModel = model; + } + + willDestroy() { + super.willDestroy(); + const config = this.configModel; + // error is thrown when you attempt to unload a record that is inFlight (isSaving) + if ((config?.isNew || config?.hasDirtyAttributes) && !config?.isSaving) { + config.unloadRecord(); + } + } + + @action + onFileUploaded(file: File) { + if (!this.configModel) return; + this.configModel.pemBundle = file.value; + } + + @action submitForm(evt: HTMLElementEvent) { + evt.preventDefault(); + if (!this.configModel) return; + this.configModel + .save() + .then(() => { + this.flashMessages.success('Successfully imported the certificate.'); + this.router.transitionTo('vault.cluster.secrets.backend.pki.issuers.index'); + }) + .catch((e) => { + this.flashMessages.danger(errorMessage(e, 'Could not import the given certificate.')); + }); + } +} diff --git a/ui/lib/pki/addon/components/pki-configure-form.hbs b/ui/lib/pki/addon/components/pki-configure-form.hbs index f8b4912a0..9568616a4 100644 --- a/ui/lib/pki/addon/components/pki-configure-form.hbs +++ b/ui/lib/pki/addon/components/pki-configure-form.hbs @@ -27,7 +27,9 @@ {{/each}} - {{#if this.configType}} + {{#if (eq this.configType "import")}} + + {{else if this.configType}} {{! TODO: Forms }} {{else}} diff --git a/ui/lib/pki/addon/components/pki-role-generate.ts b/ui/lib/pki/addon/components/pki-role-generate.ts index a6dcb430e..beda63cdd 100644 --- a/ui/lib/pki/addon/components/pki-role-generate.ts +++ b/ui/lib/pki/addon/components/pki-role-generate.ts @@ -8,6 +8,7 @@ import { tracked } from '@glimmer/tracking'; import errorMessage from 'vault/utils/error-message'; import FlashMessageService from 'vault/services/flash-messages'; import DownloadService from 'vault/services/download'; +import PkiCertificateGenerateModel from 'vault/models/pki/certificate/generate'; interface Args { onSuccess: CallableFunction; @@ -15,26 +16,6 @@ interface Args { type: string; } -interface PkiCertificateGenerateModel { - name: string; - backend: string; - serialNumber: string; - certificate: string; - formFields: FormField[]; - formFieldsGroup: { - [k: string]: FormField[]; - }[]; - save: () => void; - rollbackAttributes: () => void; - unloadRecord: () => void; - destroyRecord: () => void; - canRevoke: boolean; -} -interface FormField { - name: string; - type: string; - options: unknown; -} export default class PkiRoleGenerate extends Component { @service declare readonly router: Router; @service declare readonly store: Store; diff --git a/ui/lib/pki/addon/routes/overview.js b/ui/lib/pki/addon/routes/overview.js index 96ff54942..da7bb93c3 100644 --- a/ui/lib/pki/addon/routes/overview.js +++ b/ui/lib/pki/addon/routes/overview.js @@ -12,6 +12,8 @@ export default class PkiOverviewRoute extends Route { } hasConfig() { + // When the engine is configured, it creates a default issuer. + // If the issuers list is empty, we know it hasn't been configured const endpoint = `${this.win.origin}/v1/${this.secretMountPath.currentPath}/issuers?list=true`; return this.auth .ajax(endpoint, 'GET', {}) diff --git a/ui/tests/integration/components/pki-config/import-test.js b/ui/tests/integration/components/pki-config/import-test.js new file mode 100644 index 000000000..a73c09072 --- /dev/null +++ b/ui/tests/integration/components/pki-config/import-test.js @@ -0,0 +1,18 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupEngine } from 'ember-engines/test-support'; + +module('Integration | Component | pki-config/import', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'pki'); + + test('it renders', async function (assert) { + await render(hbs``, { owner: this.engine }); + + assert.dom('[data-test-pki-config-import-form]').exists({ count: 1 }, 'Import form exists'); + assert.dom('[data-test-pki-config-save]').isNotDisabled('Save button not disabled'); + assert.dom('[data-test-pki-config-cancel]').exists({ count: 1 }, 'cancel button exists'); + }); +}); diff --git a/ui/tests/unit/adapters/pki/config/import-test.js b/ui/tests/unit/adapters/pki/config/import-test.js new file mode 100644 index 000000000..10f2decf7 --- /dev/null +++ b/ui/tests/unit/adapters/pki/config/import-test.js @@ -0,0 +1,41 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'vault/tests/helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; + +module('Unit | Adapter | pki/config/import', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.store = this.owner.lookup('service:store'); + this.secretMountPath = this.owner.lookup('service:secret-mount-path'); + this.backend = 'pki-test'; + this.secretMountPath.currentPath = this.backend; + }); + + test('it exists', function (assert) { + const adapter = this.owner.lookup('adapter:pki/config/import'); + assert.ok(adapter); + }); + + test('it should make request to correct endpoint on create', async function (assert) { + assert.expect(2); + this.server.post(`${this.backend}/issuers/import/bundle`, (url, { requestBody }) => { + assert.ok(true, `request made to correct endpoint ${url}`); + assert.deepEqual( + JSON.parse(requestBody), + { pem_bundle: 'abcdefg' }, + 'has correct payload for config/ca' + ); + return { + data: {}, + }; + }); + + await this.store + .createRecord('pki/config/import', { + pemBundle: 'abcdefg', + }) + .save(); + }); +}); diff --git a/ui/types/ember-data/types/registries/model.d.ts b/ui/types/ember-data/types/registries/model.d.ts index e7a68fcd0..cf3025cee 100644 --- a/ui/types/ember-data/types/registries/model.d.ts +++ b/ui/types/ember-data/types/registries/model.d.ts @@ -1,6 +1,12 @@ -/** - * Catch-all for ember-data. - */ -export default interface ModelRegistry { - [key: string]: any; +import Model from '@ember-data/model'; +import PkiCertificateGenerateModel from 'vault/models/pki/certificate/generate'; +import PkiConfigImportModel from 'vault/models/pki/config/import'; + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'pki/config/import': PkiConfigImportModel; + 'pki/certificate/generate': PkiCertificateGenerateModel; + // Catchall for any other models + [key: string]: any; + } } diff --git a/ui/types/vault/app-types.ts b/ui/types/vault/app-types.ts new file mode 100644 index 000000000..5369c73e5 --- /dev/null +++ b/ui/types/vault/app-types.ts @@ -0,0 +1,6 @@ +// Type that comes back from expandAttributeMeta +export interface FormField { + name: string; + type: string; + options: unknown; +} diff --git a/ui/types/vault/models/pki/certificate/generate.d.ts b/ui/types/vault/models/pki/certificate/generate.d.ts new file mode 100644 index 000000000..c46c9921c --- /dev/null +++ b/ui/types/vault/models/pki/certificate/generate.d.ts @@ -0,0 +1,13 @@ +import Model from '@ember-data/model'; +import { FormField } from 'vault/app-types'; + +export default class PkiCertificateGenerateModel extends Model { + name: string; + backend: string; + serialNumber: string; + certificate: string; + formFields: FormField[]; + formFieldsGroup: { + [k: string]: FormField[]; + }[]; +} diff --git a/ui/types/vault/models/pki/config/import.d.ts b/ui/types/vault/models/pki/config/import.d.ts new file mode 100644 index 000000000..8b1e5c1e6 --- /dev/null +++ b/ui/types/vault/models/pki/config/import.d.ts @@ -0,0 +1,9 @@ +import Model from '@ember-data/model'; + +export default class PkiConfigImportModel extends Model { + backend: string; + secretMountPath: unknown; + importFile: string; + pemBundle: string; + certificate: string; +}