diff --git a/ui/app/models/pki/certificate/base.js b/ui/app/models/pki/certificate/base.js index f83858811f90..8564bfa1951f 100644 --- a/ui/app/models/pki/certificate/base.js +++ b/ui/app/models/pki/certificate/base.js @@ -87,8 +87,8 @@ export default class PkiCertificateBaseModel extends Model { @attr('string', { masked: true }) certificate; @attr('number') expiration; @attr('string', { label: 'Issuing CA', masked: true }) issuingCa; - @attr('string') privateKey; // only returned for type=exported - @attr('string') privateKeyType; // only returned for type=exported + @attr('string', { masked: true }) privateKey; // only returned for type=exported and /issue + @attr('string') privateKeyType; // only returned for type=exported and /issue @attr('number', { formatDate: true }) revocationTime; @attr('string') serialNumber; diff --git a/ui/app/models/pki/certificate/generate.js b/ui/app/models/pki/certificate/generate.js index d5f7e30d8b5c..cdccc2cd403f 100644 --- a/ui/app/models/pki/certificate/generate.js +++ b/ui/app/models/pki/certificate/generate.js @@ -21,7 +21,18 @@ const generateFromRole = [ ], }, ]; -@withFormFields(null, generateFromRole) +// Extra fields returned on the /issue endpoint +const certDisplayFields = [ + 'certificate', + 'commonName', + 'revocationTime', + 'serialNumber', + 'caChain', + 'issuingCa', + 'privateKey', + 'privateKeyType', +]; +@withFormFields(certDisplayFields, generateFromRole) export default class PkiCertificateGenerateModel extends PkiCertificateBaseModel { getHelpUrl(backend) { return `/v1/${backend}/issue/example?help=1`; diff --git a/ui/lib/pki/addon/components/page/pki-certificate-details.hbs b/ui/lib/pki/addon/components/page/pki-certificate-details.hbs index ba79e7ad610e..4fb1d45c4046 100644 --- a/ui/lib/pki/addon/components/page/pki-certificate-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-certificate-details.hbs @@ -18,10 +18,28 @@ +{{#if @model.privateKey}} +
+ + Next steps + + The + private_key + is only available once. Make sure you copy and save it now. + + +
+{{/if}} + {{#each @model.formFields as |field|}} - {{#if (eq field.name "certificate")}} - - + {{#if field.options.masked}} + + {{else if (eq field.name "serialNumber")}} diff --git a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js index 482e21b81656..6f288b1b83d8 100644 --- a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js @@ -40,7 +40,25 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function ( }, }, }); + store.pushPayload('pki/certificate/generate', { + modelName: 'pki/certificate/generate', + data: { + certificate: '-----BEGIN CERTIFICATE-----', + ca_chain: '-----BEGIN CERTIFICATE-----', + issuer_ca: '-----BEGIN CERTIFICATE-----', + private_key: '-----BEGIN PRIVATE KEY-----', + private_key_type: 'rsa', + common_name: 'example.com Intermediate Authority', + issue_date: 1673540867000, + serial_number: id, + parsed_certificate: { + not_valid_after: 1831220897000, + not_valid_before: 1673540867000, + }, + }, + }); this.model = store.peekRecord('pki/certificate/base', id); + this.generatedModel = store.peekRecord('pki/certificate/generate', id); this.server.post('/sys/capabilities-self', () => ({ data: { @@ -50,7 +68,7 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function ( })); }); - test('it should render actions and fields', async function (assert) { + test('it should render actions and fields for base cert', async function (assert) { assert.expect(6); this.server.post('/pki/revoke', (schema, req) => { @@ -90,6 +108,56 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function ( assert.dom('[data-test-value-div="Revocation time"]').exists('Revocation time is displayed'); }); + test('it should render actions and fields for generated cert', async function (assert) { + assert.expect(10); + + this.server.post('/pki/revoke', (schema, req) => { + const data = JSON.parse(req.requestBody); + assert.strictEqual( + data.serial_number, + this.model.serialNumber, + 'Revoke request made with serial number' + ); + return { + data: { + revocation_time: 1673972804, + revocation_time_rfc3339: '2023-01-17T16:26:44.960933411Z', + }, + }; + }); + + await render(hbs``, { owner: this.engine }); + assert.dom('[data-test-cert-detail-next-steps]').exists('Private key next steps warning shows'); + assert + .dom('[data-test-component="info-table-row"]') + .exists({ count: 9 }, 'Correct number of fields render when certificate has not been revoked'); + assert + .dom('[data-test-value-div="Certificate"] [data-test-masked-input]') + .exists('Masked input renders for certificate'); + assert.dom('[data-test-value-div="Serial number"] code').exists('Serial number renders as monospace'); + assert + .dom('[data-test-value-div="CA Chain"] [data-test-masked-input]') + .exists('CA Chain shows with masked value'); + assert + .dom('[data-test-value-div="Issuing CA"] [data-test-masked-input]') + .exists('Issuing CA shows with masked value'); + assert + .dom('[data-test-value-div="Private key"] [data-test-masked-input]') + .exists('Private key shows with masked value'); + + await click('[data-test-pki-cert-download-button]'); + const { serialNumber, certificate } = this.model; + assert.ok( + this.downloadSpy.calledWith(serialNumber.replace(/(\s|:)+/g, '-'), certificate), + 'Download pem method called with correct args' + ); + + await click('[data-test-confirm-action-trigger]'); + await click('[data-test-confirm-button]'); + + assert.dom('[data-test-value-div="Revocation time"]').exists('Revocation time is displayed'); + }); + test('it should render back button', async function (assert) { assert.expect(1);