diff --git a/packages/entities/entities-certificates/docs/certificate-form.md b/packages/entities/entities-certificates/docs/certificate-form.md index b0aafa0072..fd0b9264c3 100644 --- a/packages/entities/entities-certificates/docs/certificate-form.md +++ b/packages/entities/entities-certificates/docs/certificate-form.md @@ -55,6 +55,12 @@ A form component for Certificates. - default: `undefined` - Route to return to when canceling creation of an Certificate. + - `sniListRoute`: + - type: `RouteLocationRaw` + - required: `false` + - default: `undefined` + - Route of listing SNIs. + - `workspace`: - type: `string` - required: `true` @@ -77,6 +83,14 @@ The base konnect or kongManger config. If showing the `Edit` type form, the ID of the Certificate. +#### `showSnisField` + +- type: `boolean` +- required: `false` +- default: `false` + +Whether to show the SNIs field in the form. + ### Events #### error diff --git a/packages/entities/entities-certificates/sandbox/index.ts b/packages/entities/entities-certificates/sandbox/index.ts index 5ea0d1933f..a6b63570f2 100644 --- a/packages/entities/entities-certificates/sandbox/index.ts +++ b/packages/entities/entities-certificates/sandbox/index.ts @@ -23,6 +23,11 @@ const init = async () => { props: true, component: () => import('./pages/FallbackPage.vue'), }, + { + path: '/snis', + name: 'sni-list', + component: () => import('./pages/FallbackPage.vue'), + }, { path: '/certificate-list', name: 'certificate-list', diff --git a/packages/entities/entities-certificates/sandbox/pages/CertificateFormPage.vue b/packages/entities/entities-certificates/sandbox/pages/CertificateFormPage.vue index a126501c36..ca845e2a20 100644 --- a/packages/entities/entities-certificates/sandbox/pages/CertificateFormPage.vue +++ b/packages/entities/entities-certificates/sandbox/pages/CertificateFormPage.vue @@ -11,6 +11,7 @@ @@ -48,6 +49,7 @@ const kongManagerConfig = ref({ workspace: 'default', apiBaseUrl: '/kong-manager', // For local dev server proxy cancelRoute: { name: 'certificate-list' }, + sniListRoute: { name: 'sni-list' }, }) const onError = (error: AxiosError) => { diff --git a/packages/entities/entities-certificates/src/components/CertificateForm.cy.ts b/packages/entities/entities-certificates/src/components/CertificateForm.cy.ts index 0f14b3c66f..86841f2aea 100644 --- a/packages/entities/entities-certificates/src/components/CertificateForm.cy.ts +++ b/packages/entities/entities-certificates/src/components/CertificateForm.cy.ts @@ -6,19 +6,22 @@ import type { KongManagerCertificateFormConfig, KonnectCertificateFormConfig } f import CertificateForm from './CertificateForm.vue' const cancelRoute = { name: 'certificates-list' } +const sniListRoute = { name: 'snis-list' } -const baseConfigKonnect:KonnectCertificateFormConfig = { +const baseConfigKonnect: KonnectCertificateFormConfig = { app: 'konnect', controlPlaneId: '1234-abcd-ilove-cats', apiBaseUrl: '/us/kong-api', cancelRoute, + sniListRoute, } -const baseConfigKM:KongManagerCertificateFormConfig = { +const baseConfigKM: KongManagerCertificateFormConfig = { app: 'kongManager', workspace: 'default', apiBaseUrl: '/kong-manager', cancelRoute, + sniListRoute, } /** @@ -120,6 +123,7 @@ describe('', () => { cy.mount(CertificateForm, { props: { config: baseConfigKM, + showSnisField: true, }, }) cy.get('.kong-ui-entities-certificates-form').should('be.visible') @@ -134,6 +138,7 @@ describe('', () => { cy.getTestId('certificate-form-key').should('be.visible') cy.getTestId('certificate-form-cert-alt').should('be.visible') cy.getTestId('certificate-form-key-alt').should('be.visible') + cy.getTestId('sni-field-input-1').should('be.visible') cy.getTestId('certificate-form-tags').should('be.visible') }) @@ -151,8 +156,8 @@ describe('', () => { cy.getTestId('form-cancel').should('be.enabled') cy.getTestId('form-submit').should('be.disabled') // enables save when required fields have values - cy.getTestId('certificate-form-cert').type(certificate1.cert) - cy.getTestId('certificate-form-key').type(certificate1.key) + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) cy.getTestId('form-submit').should('be.enabled') // disables save when required field is cleared cy.getTestId('certificate-form-cert').clear() @@ -279,12 +284,12 @@ describe('', () => { cy.getTestId('form-cancel').should('be.enabled') cy.getTestId('form-submit').should('be.disabled') // enables save when required fields have values - cy.getTestId('certificate-form-cert').type(certificate1.cert) - cy.getTestId('certificate-form-key').type(certificate1.key) + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) // replaces all the newlines with spaces; this should fail the validation - cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert.replaceAll('\n', ' ')) - cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key.replaceAll('\n', ' ')) + cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert.replaceAll('\n', ' '), { delay: 0 }) + cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key.replaceAll('\n', ' '), { delay: 0 }) cy.getTestId('form-submit').should('be.enabled') @@ -312,12 +317,43 @@ describe('', () => { cy.getTestId('form-cancel').should('be.enabled') cy.getTestId('form-submit').should('be.disabled') // enables save when required fields have values - cy.getTestId('certificate-form-cert').type(certificate1.cert) - cy.getTestId('certificate-form-key').type(certificate1.key) + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) // replaces all the newlines with spaces; this should fail the validation - cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert) - cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key) + cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert, { delay: 0 }) + cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key, { delay: 0 }) + + cy.getTestId('form-submit').should('be.enabled') + + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) + + cy.wait('@validateCertificate').its('response.statusCode').should('eq', 200) + cy.wait('@createCertificate').its('response.statusCode').should('eq', 200) + }) + + it('should not fail when SNI is provided', () => { + interceptKM() + interceptUpdate() + + cy.mount(CertificateForm, { + props: { + config: baseConfigKM, + showSnisField: true, + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') + + cy.get('.kong-ui-entities-certificates-form').should('be.visible') + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) + cy.getTestId('sni-field-input-1').type('foo') cy.getTestId('form-submit').should('be.enabled') @@ -391,6 +427,7 @@ describe('', () => { cy.mount(CertificateForm, { props: { config: baseConfigKonnect, + showSnisField: true, }, }) @@ -406,6 +443,7 @@ describe('', () => { cy.getTestId('certificate-form-key').should('be.visible') cy.getTestId('certificate-form-cert-alt').should('be.visible') cy.getTestId('certificate-form-key-alt').should('be.visible') + cy.getTestId('sni-field-input-1').should('be.visible') cy.getTestId('certificate-form-tags').should('be.visible') }) @@ -422,8 +460,8 @@ describe('', () => { cy.getTestId('form-cancel').should('be.enabled') cy.getTestId('form-submit').should('be.disabled') // enables save when required fields have values - cy.getTestId('certificate-form-cert').type(certificate1.cert) - cy.getTestId('certificate-form-key').type(certificate1.key) + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) cy.getTestId('form-submit').should('be.enabled') // disables save when required field is cleared cy.getTestId('certificate-form-cert').clear() @@ -550,12 +588,12 @@ describe('', () => { cy.getTestId('form-cancel').should('be.enabled') cy.getTestId('form-submit').should('be.disabled') // enables save when required fields have values - cy.getTestId('certificate-form-cert').type(certificate1.cert) - cy.getTestId('certificate-form-key').type(certificate1.key) + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) // replaces all the newlines with spaces; this should fail the validation - cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert.replaceAll('\n', ' ')) - cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key.replaceAll('\n', ' ')) + cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert.replaceAll('\n', ' '), { delay: 0 }) + cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key.replaceAll('\n', ' '), { delay: 0 }) cy.getTestId('form-submit').should('be.enabled') @@ -583,12 +621,43 @@ describe('', () => { cy.getTestId('form-cancel').should('be.enabled') cy.getTestId('form-submit').should('be.disabled') // enables save when required fields have values - cy.getTestId('certificate-form-cert').type(certificate1.cert) - cy.getTestId('certificate-form-key').type(certificate1.key) + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) // replaces all the newlines with spaces; this should fail the validation - cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert) - cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key) + cy.getTestId('certificate-form-cert-alt').type(secp384r1CertKeyPair.cert, { delay: 0 }) + cy.getTestId('certificate-form-key-alt').type(secp384r1CertKeyPair.key, { delay: 0 }) + + cy.getTestId('form-submit').should('be.enabled') + + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) + + cy.wait('@validateCertificate').its('response.statusCode').should('eq', 200) + cy.wait('@createCertificate').its('response.statusCode').should('eq', 200) + }) + + it('should not fail when SNI is provided', () => { + interceptKonnect() + interceptUpdate() + + cy.mount(CertificateForm, { + props: { + config: baseConfigKonnect, + showSnisField: true, + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') + + cy.get('.kong-ui-entities-certificates-form').should('be.visible') + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('certificate-form-cert').type(certificate1.cert, { delay: 0 }) + cy.getTestId('certificate-form-key').type(certificate1.key, { delay: 0 }) + cy.getTestId('sni-field-input-1').type('foo') cy.getTestId('form-submit').should('be.enabled') diff --git a/packages/entities/entities-certificates/src/components/CertificateForm.vue b/packages/entities/entities-certificates/src/components/CertificateForm.vue index 44392ee1f5..a48dd44940 100644 --- a/packages/entities/entities-certificates/src/components/CertificateForm.vue +++ b/packages/entities/entities-certificates/src/components/CertificateForm.vue @@ -99,6 +99,15 @@ + + ({ key: '', certAlt: '', keyAlt: '', + snis: [''], tags: '', }, isReadonly: false, @@ -191,6 +208,7 @@ const formFieldsOriginal = reactive({ key: '', certAlt: '', keyAlt: '', + snis: [''], tags: '', }) @@ -205,6 +223,7 @@ const initForm = (data: Record): void => { form.fields.key = data?.key || '' form.fields.certAlt = data?.cert_alt || '' form.fields.keyAlt = data?.key_alt || '' + form.fields.snis = data?.snis?.length ? data.snis : [''] form.fields.tags = data?.tags?.join(', ') || '' // Set initial state of `formFieldsOriginal` to these values in order to detect changes @@ -215,6 +234,14 @@ const handleClickCancel = (): void => { router.push(props.config.cancelRoute) } +const handleAddSni = (): void => { + form.fields.snis.push('') +} + +const handleRemoveSni = (index: number): void => { + form.fields.snis.splice(index, 1) +} + /* --------------- * Saving * --------------- @@ -259,6 +286,7 @@ const requestBody = computed((): Record => { key: form.fields.key, cert_alt: form.fields.certAlt || null, key_alt: form.fields.keyAlt || null, + snis: form.fields.snis.filter(Boolean), tags: form.fields.tags.split(',')?.map((tag: string) => String(tag || '').trim())?.filter((tag: string) => tag !== ''), } }) @@ -285,6 +313,7 @@ const saveFormData = async (): Promise => { form.fields.key = response?.data?.key || '' form.fields.certAlt = response?.data?.cert_alt || '' form.fields.keyAlt = response?.data?.key_alt || '' + form.fields.snis = response?.data?.snis?.length ? response.data.snis : [''] form.fields.tags = response?.data?.tags?.join(', ') || '' // Set initial state of `formFieldsOriginal` to these values in order to detect changes diff --git a/packages/entities/entities-certificates/src/components/CertificateFormSniField.vue b/packages/entities/entities-certificates/src/components/CertificateFormSniField.vue new file mode 100644 index 0000000000..c1c800dba8 --- /dev/null +++ b/packages/entities/entities-certificates/src/components/CertificateFormSniField.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/packages/entities/entities-certificates/src/locales/en.json b/packages/entities/entities-certificates/src/locales/en.json index d54ddb6252..3cea32123d 100644 --- a/packages/entities/entities-certificates/src/locales/en.json +++ b/packages/entities/entities-certificates/src/locales/en.json @@ -51,6 +51,14 @@ "tooltip": "PEM-encoded private key of the alternate SSL key pair. This should only be set if you have both RSA and ECDSA types of certificate available and would like Kong to prefer serving using ECDSA certs when client advertises support for it. This field is {emphasis}, which means it can be securely stored as a secret in a vault. References must follow a specific format.", "emphasis": "referenceable" }, + "snis": { + "label": "SNIs", + "placeholder": "Enter an SNI", + "add": "Add an SNI", + "tooltip": "Zero or more hostnames to associate with this certificate as SNIs", + "editingTip": "To manage SNIs for this certificate, click {link}.", + "editingTipLink": "here" + }, "tags": { "label": "Tags", "placeholder": "Enter a list of tags separated by comma", diff --git a/packages/entities/entities-certificates/src/types/certificate-form.ts b/packages/entities/entities-certificates/src/types/certificate-form.ts index 76ceb1c440..4df51c39dd 100644 --- a/packages/entities/entities-certificates/src/types/certificate-form.ts +++ b/packages/entities/entities-certificates/src/types/certificate-form.ts @@ -5,12 +5,16 @@ import type { KonnectBaseFormConfig, KongManagerBaseFormConfig } from '@kong-ui- export interface KonnectCertificateFormConfig extends KonnectBaseFormConfig { /** Route to return to if canceling create/edit a certificate */ cancelRoute: RouteLocationRaw + /** Route of listing SNIs */ + sniListRoute?: RouteLocationRaw } /** Kong Manager certificate form config */ export interface KongManagerCertificateFormConfig extends KongManagerBaseFormConfig { /** Route to return to if canceling create/edit a certificate */ cancelRoute: RouteLocationRaw + /** Route of listing SNIs */ + sniListRoute?: RouteLocationRaw } export interface CertificateFormFields { @@ -18,6 +22,7 @@ export interface CertificateFormFields { key: string certAlt: string keyAlt: string + snis: Array tags: string }