diff --git a/.changeset/ui-saml-idp-certificates.md b/.changeset/ui-saml-idp-certificates.md new file mode 100644 index 00000000000..5a71c64118b --- /dev/null +++ b/.changeset/ui-saml-idp-certificates.md @@ -0,0 +1,6 @@ +--- +'@clerk/ui': minor +'@clerk/localizations': minor +--- + +The self-serve SSO configuration (`` enterprise connection page and the `ConfigureSSO` SAML steps) now shows every IdP signing certificate a SAML connection trusts, with its expiry, and lets an admin add certificates from a file (a PEM bundle adds several) or remove one, so an IdP key rotation no longer needs a replacement timed with the IdP. Saving sends the whole list and only when it changed. The list holds at most five certificates. New `configureSSOCertificateList*` and `configureSSOCertificatePrimaryBadge` appearance element descriptors cover the list, and new `configureSSO.signingCertificates.*` localization keys hold its strings. diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index 51eb8049416..0665f4b68d0 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -897,6 +897,17 @@ export const arSA: LocalizationResource = { title: 'اختر المزود', warning: 'بمجرد اختيار المزود لا يمكنك التغيير مرة أخرى حتى انتهاء التكوين', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1335,6 +1346,10 @@ export const arSA: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index 2d3047732f7..e4ad0e6c004 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -898,6 +898,17 @@ export const beBY: LocalizationResource = { title: 'Выберыце правайдэра', warning: 'Пасля выбару правайдэра вы не зможаце змяніць яго, пакуль не скончыце канфігурацыю', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1338,6 +1349,10 @@ export const beBY: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 97e0dea7866..717ee166f6e 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -899,6 +899,17 @@ export const bgBG: LocalizationResource = { title: 'Изберете доставчик', warning: 'След като изберете доставчик, не можете да го промените, докато конфигурацията не приключи', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1338,6 +1349,10 @@ export const bgBG: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index e4903dd4680..022665b1f17 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -904,6 +904,17 @@ export const bnIN: LocalizationResource = { title: 'প্রদানকারী নির্বাচন করুন', warning: 'একবার প্রদানকারী নির্বাচন করার পরে, কনফিগারেশন শেষ না হওয়া পর্যন্ত আপনি আবার পরিবর্তন করতে পারবেন না', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1345,6 +1356,10 @@ export const bnIN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 415dfb5651c..6fecadbf40b 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -905,6 +905,17 @@ export const caES: LocalizationResource = { title: 'Seleccioneu un proveïdor', warning: 'Un cop seleccionat un proveïdor no podreu canviar-lo fins que la configuració hagi finalitzat', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1345,6 +1356,10 @@ export const caES: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index be7ddf037c6..2050a63cde1 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -902,6 +902,17 @@ export const csCZ: LocalizationResource = { title: 'Vyberte poskytovatele', warning: 'Jakmile vyberete poskytovatele, nelze ho změnit, dokud nebude konfigurace dokončena', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1343,6 +1354,10 @@ export const csCZ: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index e9b3b4facda..863123bccb2 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -897,6 +897,17 @@ export const daDK: LocalizationResource = { title: 'Vælg udbyder', warning: 'Når en udbyder er valgt, kan du ikke ændre den, før konfigurationen er færdig', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1336,6 +1347,10 @@ export const daDK: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 85211a28986..3dae5675b44 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -906,6 +906,17 @@ export const deDE: LocalizationResource = { warning: 'Sobald ein Anbieter ausgewählt ist, können Sie ihn nicht mehr ändern, bis die Konfiguration abgeschlossen ist', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1351,6 +1362,10 @@ export const deDE: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 0467f20fb50..cbaa83a14eb 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -898,6 +898,17 @@ export const elGR: LocalizationResource = { title: 'Επιλέξτε πάροχο', warning: 'Μόλις επιλεγεί ένας πάροχος δεν μπορείτε να τον αλλάξετε μέχρι να ολοκληρωθεί η ρύθμιση', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1341,6 +1352,10 @@ export const elGR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index e028cff9e6f..8cf3fa19f17 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -897,6 +897,17 @@ export const enGB: LocalizationResource = { title: 'Select provider', warning: 'Once a provider is selected you cannot change again until the configuration is over', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const enGB: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index abbcb744c3f..34ba72cb85c 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -500,7 +500,7 @@ export const enUS: LocalizationResource = { }, signingCertificate: { fileUploaded: 'File uploaded', - label: 'Signing certificate', + label: 'Signing certificates', removeFile: 'Remove file', replaceFile: 'Replace file', uploadFile: 'Upload file', @@ -583,7 +583,7 @@ export const enUS: LocalizationResource = { }, signingCertificate: { fileUploaded: 'File uploaded', - label: 'Signing certificate', + label: 'Signing certificates', removeFile: 'Remove file', replaceFile: 'Replace file', uploadFile: 'Upload file', @@ -700,7 +700,7 @@ export const enUS: LocalizationResource = { }, signingCertificate: { fileUploaded: 'File uploaded', - label: 'Signing certificate', + label: 'Signing certificates', removeFile: 'Remove file', replaceFile: 'Replace file', uploadFile: 'Upload file', @@ -822,7 +822,7 @@ export const enUS: LocalizationResource = { }, signingCertificate: { fileUploaded: 'File uploaded', - label: 'Signing certificate', + label: 'Signing certificates', removeFile: 'Remove file', replaceFile: 'Replace file', uploadFile: 'Upload file', @@ -926,6 +926,17 @@ export const enUS: LocalizationResource = { title: 'Select your identity provider', warning: 'Once a provider is selected you cannot change again until the configuration is over', }, + signingCertificates: { + addCertificate: 'Add certificate', + expired: 'Expired {{date}}', + expires: 'Expires {{date}}', + expiryAfterSave: 'Expiry shows after you save.', + fileUnreadable: 'The file could not be read. Try again.', + notACertificate: 'One of the uploaded files is not a certificate.', + primary: 'Primary', + primaryTooltip: 'Checked first when verifying a sign-in. Every certificate in this list is trusted.', + removeCertificate: 'Remove certificate', + }, testConfigurationStep: { error__noSuccessfulTestRun: 'You need at least one successful test run before you can continue. Generate a test URL and complete the sign-in flow.', @@ -1375,6 +1386,10 @@ export const enUS: LocalizationResource = { }, identityProvider: { certificateExpires: 'Certificate expires', + certificates: 'Certificates', + certificatesCount: '{{count}} certificates', + certificatesSummary: '{{count}} certificates, earliest expires {{date}}', + certificatesSummaryExpired: '{{count}} certificates, one expired {{date}}', clientSecret: { placeholder: 'Leave empty to keep the current secret', }, diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index f13e61b1055..881651ad605 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -898,6 +898,17 @@ export const esCR: LocalizationResource = { title: 'Seleccionar proveedor', warning: 'Una vez que se selecciona un proveedor no puedes cambiarlo hasta que termine la configuración', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1341,6 +1352,10 @@ export const esCR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 175e88c7ef6..352b491767c 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -904,6 +904,17 @@ export const esES: LocalizationResource = { title: 'Seleccionar proveedor', warning: 'Una vez seleccionado un proveedor no podrás cambiarlo hasta que finalice la configuración', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1346,6 +1357,10 @@ export const esES: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index 35a9eab3747..fb8f136a774 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -899,6 +899,17 @@ export const esMX: LocalizationResource = { title: 'Seleccionar proveedor', warning: 'Una vez que se selecciona un proveedor no puedes cambiarlo hasta que termine la configuración', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1342,6 +1353,10 @@ export const esMX: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 5f03cd55126..4bc80046ddc 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -898,6 +898,17 @@ export const esUY: LocalizationResource = { title: 'Seleccionar proveedor', warning: 'Una vez que se selecciona un proveedor no podés cambiarlo hasta que termine la configuración', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1340,6 +1351,10 @@ export const esUY: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index 72e0ee3f716..1cec93c532a 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -903,6 +903,17 @@ export const faIR: LocalizationResource = { title: 'ارائه‌دهنده را انتخاب کنید', warning: 'پس از انتخاب یک ارائه‌دهنده، نمی‌توانید آن را تا پایان پیکربندی تغییر دهید', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1345,6 +1356,10 @@ export const faIR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index c3b07cb926e..af968a9348d 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -904,6 +904,17 @@ export const fiFI: LocalizationResource = { title: 'Valitse palveluntarjoaja', warning: 'Kun palveluntarjoaja on valittu, et voi vaihtaa sitä ennen kuin määritys on valmis', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1346,6 +1357,10 @@ export const fiFI: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index 31685940074..f10b2f0e17e 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -907,6 +907,17 @@ export const frFR: LocalizationResource = { warning: "Une fois un fournisseur sélectionné, vous ne pourrez plus en changer jusqu'à la fin de la configuration", }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1351,6 +1362,10 @@ export const frFR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 0c49ef326f3..9238c575468 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -897,6 +897,17 @@ export const heIL: LocalizationResource = { title: 'בחר ספק', warning: 'לאחר בחירת ספק לא ניתן לשנות אותו עד לסיום ההגדרה', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1332,6 +1343,10 @@ export const heIL: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 3b2df157c4a..50faad7f25b 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -904,6 +904,17 @@ export const hiIN: LocalizationResource = { title: 'प्रदाता चुनें', warning: 'एक बार प्रदाता का चयन करने के बाद आप कॉन्फ़िगरेशन समाप्त होने तक इसे बदल नहीं सकते', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1345,6 +1356,10 @@ export const hiIN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index 40c4423cc1c..d4f315fc89c 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -905,6 +905,17 @@ export const hrHR: LocalizationResource = { title: 'Odaberite pružatelja', warning: 'Nakon odabira pružatelja ne možete ga ponovno mijenjati dok konfiguracija ne završi', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1346,6 +1357,10 @@ export const hrHR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index bd0aeb00fc0..87f03437062 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -906,6 +906,17 @@ export const huHU: LocalizationResource = { title: 'Szolgáltató kiválasztása', warning: 'Miután kiválasztotta a szolgáltatót, nem módosíthatja, amíg a konfiguráció be nem fejeződik', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1347,6 +1358,10 @@ export const huHU: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index bb8f7a58b70..4a56e4e5711 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -898,6 +898,17 @@ export const idID: LocalizationResource = { title: 'Pilih penyedia', warning: 'Setelah penyedia dipilih, Anda tidak dapat mengubahnya lagi sampai konfigurasi selesai', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1339,6 +1350,10 @@ export const idID: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index f53f4d6cff5..78dc41a170e 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -904,6 +904,17 @@ export const isIS: LocalizationResource = { title: 'Veldu þjónustuaðila', warning: 'Þegar þjónustuaðili hefur verið valinn er ekki hægt að breyta aftur fyrr en stillingu er lokið', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1346,6 +1357,10 @@ export const isIS: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 826889a94b4..bb419bfd479 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -904,6 +904,17 @@ export const itIT: LocalizationResource = { title: 'Seleziona provider', warning: 'Una volta selezionato un provider non potrai cambiarlo fino al termine della configurazione', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1346,6 +1357,10 @@ export const itIT: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index c2cdb2926ad..4f1fe4d90ce 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -905,6 +905,17 @@ export const jaJP: LocalizationResource = { title: 'プロバイダーを選択', warning: 'プロバイダーを選択すると、設定が完了するまで変更できません', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1345,6 +1356,10 @@ export const jaJP: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index 306bd58ce3c..f669637237f 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -898,6 +898,17 @@ export const kkKZ: LocalizationResource = { title: 'Провайдерді таңдау', warning: 'Провайдер таңдалғаннан кейін, конфигурация аяқталғанша өзгерте алмайсыз', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1332,6 +1343,10 @@ export const kkKZ: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 8b76029443a..88e554eaefa 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -901,6 +901,17 @@ export const koKR: LocalizationResource = { title: '공급자 선택', warning: '공급자를 선택하면 구성이 완료될 때까지 다시 변경할 수 없습니다', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const koKR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index 375c45efd4a..6aa4840b5a8 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -898,6 +898,17 @@ export const mnMN: LocalizationResource = { title: 'Үйлчилгээ үзүүлэгчийг сонгох', warning: 'Үйлчилгээ үзүүлэгчийг сонгосны дараа тохиргоо дуустал өөрчлөх боломжгүй', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1339,6 +1350,10 @@ export const mnMN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index ed21fbaef48..7c53a9ae9c3 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -906,6 +906,17 @@ export const msMY: LocalizationResource = { title: 'Pilih pembekal', warning: 'Setelah pembekal dipilih anda tidak boleh menukar lagi sehingga konfigurasi selesai', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1348,6 +1359,10 @@ export const msMY: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 9e42aac4b76..c31ca1ffa75 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -905,6 +905,17 @@ export const nbNO: LocalizationResource = { title: 'Velg leverandør', warning: 'Når en leverandør er valgt, kan du ikke endre igjen før konfigurasjonen er ferdig', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1347,6 +1358,10 @@ export const nbNO: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index ddd60976415..69a5f136b18 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -898,6 +898,17 @@ export const nlBE: LocalizationResource = { title: 'Provider selecteren', warning: 'Zodra een provider is geselecteerd, kun je deze niet meer wijzigen totdat de configuratie is voltooid', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1339,6 +1350,10 @@ export const nlBE: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index e2a1969b441..27da0499ce9 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -898,6 +898,17 @@ export const nlNL: LocalizationResource = { title: 'Provider selecteren', warning: 'Zodra een provider is geselecteerd, kun je deze niet meer wijzigen totdat de configuratie is voltooid', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1339,6 +1350,10 @@ export const nlNL: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 76d3adc7ea9..04af979284e 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -898,6 +898,17 @@ export const plPL: LocalizationResource = { title: 'Wybierz dostawcę', warning: 'Po wybraniu dostawcy nie można go ponownie zmienić aż do zakończenia konfiguracji', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const plPL: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index 0d9ac6023a7..99b368031c2 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -905,6 +905,17 @@ export const ptBR: LocalizationResource = { warning: 'Depois que um provedor for selecionado, você não poderá alterá-lo até que a configuração seja concluída', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1347,6 +1358,10 @@ export const ptBR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 4440c052473..48d6a572489 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -906,6 +906,17 @@ export const ptPT: LocalizationResource = { title: 'Selecionar fornecedor', warning: 'Depois de um fornecedor ser selecionado não pode ser alterado até que a configuração esteja terminada', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1347,6 +1358,10 @@ export const ptPT: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index b372cf79c68..be1e270ce09 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -905,6 +905,17 @@ export const roRO: LocalizationResource = { title: 'Selectați furnizorul', warning: 'Odată ce un furnizor este selectat, nu îl puteți schimba până când configurația nu este finalizată', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1347,6 +1358,10 @@ export const roRO: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 07d081dd992..4403998a770 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -898,6 +898,17 @@ export const ruRU: LocalizationResource = { title: 'Выберите поставщика', warning: 'После выбора поставщика вы не сможете изменить его до завершения настройки', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1341,6 +1352,10 @@ export const ruRU: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index fc9169d88d2..c73b35e563c 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -898,6 +898,17 @@ export const skSK: LocalizationResource = { title: 'Vyberte poskytovateľa', warning: 'Po výbere poskytovateľa ho nemôžete zmeniť, kým sa konfigurácia neukončí', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const skSK: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index cc7fbb83dfb..e43def61292 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -898,6 +898,17 @@ export const srRS: LocalizationResource = { title: 'Izaberite provajdera', warning: 'Kada se provajder izabere, ne možete ga ponovo menjati dok se konfiguracija ne završi', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const srRS: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 4277bb57a62..31dfc362a9e 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -898,6 +898,17 @@ export const svSE: LocalizationResource = { title: 'Välj leverantör', warning: 'När en leverantör har valts kan du inte ändra igen förrän konfigurationen är klar', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const svSE: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index 8f8ee372d62..71ba727aadc 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -906,6 +906,17 @@ export const taIN: LocalizationResource = { title: 'வழங்குநரைத் தேர்ந்தெடுக்கவும்', warning: 'வழங்குநரைத் தேர்ந்தெடுத்த பிறகு, கட்டமைப்பு முடியும் வரை மீண்டும் மாற்ற முடியாது', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1350,6 +1361,10 @@ export const taIN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index 957f053751c..e5bf22ca4cd 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -905,6 +905,17 @@ export const teIN: LocalizationResource = { title: 'ప్రొవైడర్‌ను ఎంచుకోండి', warning: 'ఒకసారి ప్రొవైడర్ ఎంచుకున్న తర్వాత, కాన్ఫిగరేషన్ ముగిసే వరకు మీరు మళ్లీ మార్చలేరు', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1347,6 +1358,10 @@ export const teIN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index c98233dba9f..25185bfbb26 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -901,6 +901,17 @@ export const thTH: LocalizationResource = { title: 'เลือกผู้ให้บริการ', warning: 'เมื่อเลือกผู้ให้บริการแล้วคุณไม่สามารถเปลี่ยนได้อีกจนกว่าการกำหนดค่าจะเสร็จสิ้น', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1338,6 +1349,10 @@ export const thTH: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index fe69dcdc525..addbef60fef 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -898,6 +898,17 @@ export const trTR: LocalizationResource = { title: 'Sağlayıcıyı seçin', warning: 'Bir sağlayıcı seçildikten sonra yapılandırma bitene kadar tekrar değiştiremezsiniz', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const trTR: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index e3a16b8ea27..f714296df0c 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -898,6 +898,17 @@ export const ukUA: LocalizationResource = { title: 'Виберіть постачальника', warning: 'Після вибору постачальника ви не зможете змінити його, доки не буде завершено налаштування', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1337,6 +1348,10 @@ export const ukUA: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index 8c543c1e108..f4af25fadb9 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -904,6 +904,17 @@ export const viVN: LocalizationResource = { title: 'Chọn nhà cung cấp', warning: 'Khi đã chọn nhà cung cấp, bạn không thể thay đổi cho đến khi cấu hình hoàn tất', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1345,6 +1356,10 @@ export const viVN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 2537e98bae0..429fba2d4d1 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -897,6 +897,17 @@ export const zhCN: LocalizationResource = { title: '选择提供商', warning: '选择提供商后,在配置完成之前无法再次更改', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1330,6 +1341,10 @@ export const zhCN: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index a8db934fa49..0a59569f066 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -900,6 +900,17 @@ export const zhTW: LocalizationResource = { title: '選擇提供者', warning: '選擇提供者後,在設定完成之前無法再次變更', }, + signingCertificates: { + addCertificate: undefined, + expired: undefined, + expires: undefined, + expiryAfterSave: undefined, + fileUnreadable: undefined, + notACertificate: undefined, + primary: undefined, + primaryTooltip: undefined, + removeCertificate: undefined, + }, testConfigurationStep: { error__noSuccessfulTestRun: undefined, subtitle: undefined, @@ -1333,6 +1344,10 @@ export const zhTW: LocalizationResource = { }, identityProvider: { certificateExpires: undefined, + certificates: undefined, + certificatesCount: undefined, + certificatesSummary: undefined, + certificatesSummaryExpired: undefined, clientSecret: { placeholder: undefined, }, diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 588bd91a29f..912ac54e607 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1302,6 +1302,10 @@ export type __internal_LocalizationResource = { identityProvider: { title: LocalizationValue; certificateExpires: LocalizationValue; + certificates: LocalizationValue; + certificatesCount: LocalizationValue<'count'>; + certificatesSummary: LocalizationValue<'count' | 'date'>; + certificatesSummaryExpired: LocalizationValue<'count' | 'date'>; editButton: LocalizationValue; form: { title: LocalizationValue; @@ -2259,6 +2263,17 @@ export type __internal_LocalizationResource = { }; }; }; + signingCertificates: { + addCertificate: LocalizationValue; + expired: LocalizationValue<'date'>; + expires: LocalizationValue<'date'>; + expiryAfterSave: LocalizationValue; + fileUnreadable: LocalizationValue; + notACertificate: LocalizationValue; + primary: LocalizationValue; + primaryTooltip: LocalizationValue; + removeCertificate: LocalizationValue; + }; activate: { title: LocalizationValue; subtitle: LocalizationValue<'domain'>; diff --git a/packages/ui/src/components/ConfigureSSO/domain/__tests__/idpCertificates.test.ts b/packages/ui/src/components/ConfigureSSO/domain/__tests__/idpCertificates.test.ts new file mode 100644 index 00000000000..29ef2f5a37e --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/domain/__tests__/idpCertificates.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest'; + +import { + addCertificates, + areCertificateBodies, + getIdpCertificateStatus, + haveCertificatesChanged, + parseCertificateFile, + removeCertificate, + toIdpCertificateEntries, + toIdpCertificatesParam, +} from '../idpCertificates'; + +const pem = (body: string) => `-----BEGIN CERTIFICATE-----\n${body}\n-----END CERTIFICATE-----\n`; + +const entry = (certificate: string, expiresAt: number | null = null) => ({ + certificate, + issuedAt: null, + expiresAt, +}); + +describe('toIdpCertificateEntries', () => { + it('prefers the list over the single certificate', () => { + const list = [entry('a', 1), entry('b', 2)]; + expect(toIdpCertificateEntries({ idpCertificate: 'a', idpCertificates: list })).toBe(list); + }); + + it('falls back to the single certificate and its validity columns', () => { + expect(toIdpCertificateEntries({ idpCertificate: 'a' })).toEqual([entry('a')]); + expect(toIdpCertificateEntries({ idpCertificate: 'a', idpCertificates: [] })).toEqual([entry('a')]); + expect( + toIdpCertificateEntries({ idpCertificate: 'a', idpCertificateIssuedAt: 1, idpCertificateExpiresAt: 2 }), + ).toEqual([{ certificate: 'a', issuedAt: 1, expiresAt: 2 }]); + expect(toIdpCertificateEntries({ idpCertificate: 'a', idpCertificateExpiresAt: 0 })).toEqual([entry('a')]); + }); + + it('is empty without a connection or certificate', () => { + expect(toIdpCertificateEntries(null)).toEqual([]); + expect(toIdpCertificateEntries({ idpCertificate: '' })).toEqual([]); + }); +}); + +describe('parseCertificateFile', () => { + it('reads one PEM certificate', () => { + expect(parseCertificateFile(pem('AAAA\nBBBB'))).toEqual(['AAAABBBB']); + }); + + it('reads every block of a PEM bundle, ignoring text between blocks', () => { + expect(parseCertificateFile(`subject=CN=a\n${pem('AAAA')}subject=CN=b\r\n${pem('BBBB')}`)).toEqual([ + 'AAAA', + 'BBBB', + ]); + }); + + it('reads a bare base64 certificate with whitespace', () => { + expect(parseCertificateFile(' AAAA\n BBBB\n')).toEqual(['AAAABBBB']); + }); + + it('is empty for an empty file', () => { + expect(parseCertificateFile(' ')).toEqual([]); + }); +}); + +describe('areCertificateBodies', () => { + it('accepts base64 bodies and rejects anything else, such as a private key', () => { + expect(areCertificateBodies(['MIICozCCAYs=', 'AAAA'])).toBe(true); + expect(areCertificateBodies([])).toBe(false); + expect( + areCertificateBodies(parseCertificateFile('-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n')), + ).toBe(false); + }); +}); + +describe('addCertificates', () => { + it('appends new certificates and skips ones already in the list', () => { + expect(addCertificates([entry('AAAA', 1)], ['AAAA', 'BBBB'])).toEqual([entry('AAAA', 1), entry('BBBB')]); + }); + + it('returns the same list when nothing is new', () => { + const entries = [entry('AAAA', 1)]; + expect(addCertificates(entries, ['AAAA'])).toBe(entries); + }); + + it('adds a bundle in order without duplicates', () => { + expect(addCertificates([], ['AAAA', 'BBBB', 'AAAA'])).toEqual([entry('AAAA'), entry('BBBB')]); + }); + + it('never grows the list past the maximum', () => { + const four = ['A', 'B', 'C', 'D'].map(c => entry(c)); + expect(addCertificates(four, ['E', 'F'])).toEqual([...four, entry('E')]); + expect(addCertificates([...four, entry('E')], ['F'])).toEqual([...four, entry('E')]); + }); +}); + +describe('removeCertificate', () => { + it('drops the matching entry', () => { + expect(removeCertificate([entry('a'), entry('b')], 'a')).toEqual([entry('b')]); + }); +}); + +describe('haveCertificatesChanged', () => { + const original = [entry('a', 1), entry('b', 2)]; + + it('is false for the same certificates in the same order', () => { + expect(haveCertificatesChanged([entry('a'), entry('b')], original)).toBe(false); + }); + + it('is true when one is added, removed, or reordered', () => { + expect(haveCertificatesChanged([entry('a'), entry('b'), entry('c')], original)).toBe(true); + expect(haveCertificatesChanged([entry('a')], original)).toBe(true); + expect(haveCertificatesChanged([entry('b'), entry('a')], original)).toBe(true); + }); +}); + +describe('toIdpCertificatesParam', () => { + it('sends the certificate bodies in order', () => { + expect(toIdpCertificatesParam([entry('a'), entry('b')])).toEqual(['a', 'b']); + }); +}); + +describe('getIdpCertificateStatus', () => { + const now = Date.UTC(2026, 8, 30); + const day = 24 * 60 * 60 * 1000; + + it('classifies by the expiry date', () => { + expect(getIdpCertificateStatus(entry('a'), now)).toBe('unknown'); + expect(getIdpCertificateStatus(entry('a', now - day), now)).toBe('expired'); + expect(getIdpCertificateStatus(entry('a', now), now)).toBe('expired'); + expect(getIdpCertificateStatus(entry('a', now + 10 * day), now)).toBe('expiring'); + expect(getIdpCertificateStatus(entry('a', now + 31 * day), now)).toBe('valid'); + }); +}); diff --git a/packages/ui/src/components/ConfigureSSO/domain/idpCertificates.ts b/packages/ui/src/components/ConfigureSSO/domain/idpCertificates.ts new file mode 100644 index 00000000000..900f34106e5 --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/domain/idpCertificates.ts @@ -0,0 +1,108 @@ +import type { EnterpriseSamlConnectionNestedResource, EnterpriseSamlIdpCertificateResource } from '@clerk/shared/types'; + +export type IdpCertificateEntry = EnterpriseSamlIdpCertificateResource; + +export type IdpCertificateStatus = 'expired' | 'expiring' | 'valid' | 'unknown'; + +export const EXPIRY_WARNING_DAYS = 30; + +export const MAX_IDP_CERTIFICATES = 5; + +const PEM_HEADER = '-----BEGIN CERTIFICATE-----'; +const PEM_FOOTER = '-----END CERTIFICATE-----'; +const BASE64_BODY = /^[A-Za-z0-9+/]+=*$/; + +type SamlCertificateSource = Partial< + Pick< + EnterpriseSamlConnectionNestedResource, + 'idpCertificate' | 'idpCertificateIssuedAt' | 'idpCertificateExpiresAt' | 'idpCertificates' + > +>; + +/** + * The certificates a connection trusts, primary first. A connection read + * without `idpCertificates` only carries the single certificate and its + * validity columns, which become one entry. + */ +export function toIdpCertificateEntries(saml: SamlCertificateSource | null | undefined): IdpCertificateEntry[] { + if (saml?.idpCertificates?.length) { + return saml.idpCertificates; + } + if (saml?.idpCertificate) { + return [ + { + certificate: saml.idpCertificate, + issuedAt: saml.idpCertificateIssuedAt || null, + expiresAt: saml.idpCertificateExpiresAt || null, + }, + ]; + } + return []; +} + +/** + * Splits an uploaded file into bare base64 certificate bodies: one per PEM + * block, or the whole file when it holds a single bare base64 certificate. + */ +export function parseCertificateFile(text: string): string[] { + const chunks = text.split(PEM_HEADER); + const bodies: string[] = []; + chunks.forEach((chunk, index) => { + if (index === 0 && chunks.length > 1) { + return; + } + const end = chunk.indexOf(PEM_FOOTER); + const body = (end >= 0 ? chunk.slice(0, end) : chunk).replace(/\s+/g, ''); + if (body) { + bodies.push(body); + } + }); + return bodies; +} + +export function areCertificateBodies(bodies: string[]): boolean { + return bodies.length > 0 && bodies.every(body => BASE64_BODY.test(body)); +} + +export function addCertificates(entries: IdpCertificateEntry[], bodies: string[]): IdpCertificateEntry[] { + const known = new Set(entries.map(entry => entry.certificate)); + const added: IdpCertificateEntry[] = []; + for (const certificate of bodies) { + if (entries.length + added.length >= MAX_IDP_CERTIFICATES) { + break; + } + if (!known.has(certificate)) { + known.add(certificate); + added.push({ certificate, issuedAt: null, expiresAt: null }); + } + } + return added.length > 0 ? [...entries, ...added] : entries; +} + +export function removeCertificate(entries: IdpCertificateEntry[], certificate: string): IdpCertificateEntry[] { + return entries.filter(entry => entry.certificate !== certificate); +} + +export function haveCertificatesChanged(entries: IdpCertificateEntry[], original: IdpCertificateEntry[]): boolean { + return ( + entries.length !== original.length || + entries.some((entry, index) => entry.certificate !== original[index].certificate) + ); +} + +export function toIdpCertificatesParam(entries: IdpCertificateEntry[]): string[] { + return entries.map(entry => entry.certificate); +} + +export function getIdpCertificateStatus(entry: IdpCertificateEntry, now: number = Date.now()): IdpCertificateStatus { + if (entry.expiresAt === null) { + return 'unknown'; + } + if (entry.expiresAt <= now) { + return 'expired'; + } + if (entry.expiresAt - now <= EXPIRY_WARNING_DAYS * 24 * 60 * 60 * 1000) { + return 'expiring'; + } + return 'valid'; +} diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx index b42c6be20e3..e3b30f0a4c0 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx @@ -21,6 +21,7 @@ import { Checkmark, Clipboard } from '@/icons'; import { useFormControl } from '@/ui/utils/useFormControl'; import { useConfigureSSO } from '../../../ConfigureSSOContext'; +import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates'; import { Step } from '../../../elements/Step'; import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard'; import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter'; @@ -326,10 +327,10 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => { samlConnection?.idpCertificate || samlConnection?.idpMetadataUrl, ); - const existingCertPresent = Boolean(samlConnection?.idpCertificate); + const initialCertificates = toIdpCertificateEntries(samlConnection); const [mode, setMode] = React.useState(hasExistingConfig ? 'manual' : 'metadataUrl'); - const [certFile, setCertFile] = React.useState(null); + const [certificates, setCertificates] = React.useState(initialCertificates); // Step-LOCAL submit state for the Continue button. `goNext` bubbles to the // parent (this is the terminal nested step) and the parent DEFERS the // configure→test advance until the updateConnection revalidate lands. Keeping @@ -378,7 +379,7 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => { const trimmedMetadataUrl = metadataUrlField.value.trim(); const trimmedSignOnUrl = signOnUrlField.value.trim(); const trimmedIssuer = issuerField.value.trim(); - const hasCert = certFile !== null || existingCertPresent; + const hasCert = certificates.length > 0; const isValid = mode === 'metadataUrl' @@ -404,9 +405,9 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => { signOnUrlField, issuerField, certificateField, - certFile, - onCertFileChange: setCertFile, - existingCertPresent, + certificates, + onCertificatesChange: setCertificates, + initialCertificates, }, labels: { description: localizationKeys( @@ -439,7 +440,7 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => { const saml = await buildSamlConfigurationPayload({ mode, metadataUrl: { value: metadataUrlField.value }, - manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile }, + manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates }, }); await updateConnection(enterpriseConnection.id, { saml }); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx index 901884d9238..5c1a74bda9e 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx @@ -9,6 +9,7 @@ import { localizationKeys } from '@/localization'; import { useFormControl } from '@/ui/utils/useFormControl'; import { useConfigureSSO } from '../../../ConfigureSSOContext'; +import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates'; import { Step } from '../../../elements/Step'; import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard'; import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter'; @@ -158,12 +159,12 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => { const hasExistingManualConfig = Boolean( samlConnection?.idpSsoUrl || samlConnection?.idpEntityId || samlConnection?.idpCertificate, ); - const existingCertPresent = Boolean(samlConnection?.idpCertificate); + const initialCertificates = toIdpCertificateEntries(samlConnection); const existingMetadataPresent = Boolean(samlConnection?.idpMetadata); const [mode, setMode] = React.useState(hasExistingManualConfig ? 'manual' : 'metadataFile'); const [metadataFile, setMetadataFile] = React.useState(null); - const [certFile, setCertFile] = React.useState(null); + const [certificates, setCertificates] = React.useState(initialCertificates); // Step-LOCAL submit state for the Continue button. `goNext` bubbles to the // parent (this is the terminal nested step) and the parent DEFERS the // configure→test advance until the updateConnection revalidate lands. Keeping @@ -208,7 +209,7 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => { const trimmedSignOnUrl = signOnUrlField.value.trim(); const trimmedIssuer = issuerField.value.trim(); - const hasCert = certFile !== null || existingCertPresent; + const hasCert = certificates.length > 0; const hasMetadataFile = metadataFile !== null || existingMetadataPresent; const isValid = @@ -250,9 +251,9 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => { signOnUrlField, issuerField, certificateField, - certFile, - onCertFileChange: setCertFile, - existingCertPresent, + certificates, + onCertificatesChange: setCertificates, + initialCertificates, }, labels: { description: localizationKeys( @@ -285,7 +286,7 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => { const saml = await buildSamlConfigurationPayload({ mode, metadataFile: { file: metadataFile }, - manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile }, + manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates }, }); await updateConnection(enterpriseConnection.id, { saml }); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx index a4e1bc94983..e82632fc3bc 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx @@ -30,6 +30,7 @@ import { truncateWithEndVisible } from '@/ui/utils/truncateTextWithEndVisible'; import { useFormControl } from '@/ui/utils/useFormControl'; import { useConfigureSSO } from '../../../ConfigureSSOContext'; +import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates'; import { Step } from '../../../elements/Step'; import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard'; import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter'; @@ -654,10 +655,10 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => { samlConnection?.idpCertificate || samlConnection?.idpMetadataUrl, ); - const existingCertPresent = Boolean(samlConnection?.idpCertificate); + const initialCertificates = toIdpCertificateEntries(samlConnection); const [mode, setMode] = React.useState(hasExistingConfig ? 'manual' : 'metadataUrl'); - const [certFile, setCertFile] = React.useState(null); + const [certificates, setCertificates] = React.useState(initialCertificates); const [isSubmitting, setIsSubmitting] = React.useState(false); const metadataUrlField = useFormControl('idpMetadataUrl', samlConnection?.idpMetadataUrl ?? '', { @@ -702,7 +703,7 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => { const trimmedMetadataUrl = metadataUrlField.value.trim(); const trimmedSignOnUrl = signOnUrlField.value.trim(); const trimmedIssuer = issuerField.value.trim(); - const hasCert = certFile !== null || existingCertPresent; + const hasCert = certificates.length > 0; const isValid = mode === 'metadataUrl' @@ -728,9 +729,9 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => { signOnUrlField, issuerField, certificateField, - certFile, - onCertFileChange: setCertFile, - existingCertPresent, + certificates, + onCertificatesChange: setCertificates, + initialCertificates, }, labels: { description: localizationKeys( @@ -763,7 +764,7 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => { const saml = await buildSamlConfigurationPayload({ mode, metadataUrl: { value: metadataUrlField.value }, - manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile }, + manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates }, }); await updateConnection(enterpriseConnection.id, { saml }); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx index 39ca8214704..758303c80c4 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx @@ -22,6 +22,7 @@ import { Checkmark, Clipboard } from '@/icons'; import { useFormControl } from '@/ui/utils/useFormControl'; import { useConfigureSSO } from '../../../ConfigureSSOContext'; +import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates'; import { Step } from '../../../elements/Step'; import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard'; import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter'; @@ -512,10 +513,10 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => { samlConnection?.idpCertificate || samlConnection?.idpMetadataUrl, ); - const existingCertPresent = Boolean(samlConnection?.idpCertificate); + const initialCertificates = toIdpCertificateEntries(samlConnection); const [mode, setMode] = React.useState(hasExistingConfig ? 'manual' : 'metadataUrl'); - const [certFile, setCertFile] = React.useState(null); + const [certificates, setCertificates] = React.useState(initialCertificates); // Step-LOCAL submit state for the Continue button. `goNext` bubbles to the // parent (this is the terminal nested step) and the parent DEFERS the // configure→test advance until the updateConnection revalidate lands. Keeping @@ -562,7 +563,7 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => { const trimmedMetadataUrl = metadataUrlField.value.trim(); const trimmedSignOnUrl = signOnUrlField.value.trim(); const trimmedIssuer = issuerField.value.trim(); - const hasCert = certFile !== null || existingCertPresent; + const hasCert = certificates.length > 0; const isValid = mode === 'metadataUrl' @@ -588,9 +589,9 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => { signOnUrlField, issuerField, certificateField, - certFile, - onCertFileChange: setCertFile, - existingCertPresent, + certificates, + onCertificatesChange: setCertificates, + initialCertificates, }, labels: { description: localizationKeys( @@ -623,7 +624,7 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => { const saml = await buildSamlConfigurationPayload({ mode, metadataUrl: { value: metadataUrlField.value }, - manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile }, + manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates }, }); await updateConnection(enterpriseConnection.id, { saml }); // `goNext` bubbles to the parent, which DEFERS the advance to `test` until diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx index 0237bc7df53..7ce914627eb 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx @@ -11,16 +11,31 @@ import { Flex, Icon, type LocalizationKey, + localizationKeys, + Span, Text, useLocalizations, } from '@/customizables'; import type { useCardState } from '@/elements/contexts'; import { Field } from '@/elements/FieldControl'; import { Form } from '@/elements/Form'; -import { ArrowUpTray, Close } from '@/icons'; +import { Tooltip } from '@/elements/Tooltip'; +import { ArrowUpTray, Close, ExclamationTriangle } from '@/icons'; +import { formatDate } from '@/ui/utils/formatDate'; import type { FormControlState } from '@/ui/utils/useFormControl'; import { handleError } from '@/utils/errorHandler'; +import { + addCertificates, + areCertificateBodies, + getIdpCertificateStatus, + haveCertificatesChanged, + type IdpCertificateEntry, + MAX_IDP_CERTIFICATES, + parseCertificateFile, + removeCertificate, + toIdpCertificatesParam, +} from '../../../../domain/idpCertificates'; import type { SamlIdpConfigurationMode } from '../../shared/IdentityProviderConfigurationModes'; type CardState = ReturnType; @@ -56,9 +71,9 @@ type ManualConfigurationForm = { signOnUrlField: FormControl; issuerField: FormControl; certificateField: FormControl; - certFile: File | null; - onCertFileChange: (file: File | null) => void; - existingCertPresent?: boolean; + certificates: IdpCertificateEntry[]; + onCertificatesChange: React.Dispatch>; + initialCertificates: IdpCertificateEntry[]; }; type ManualConfigurationLabels = { @@ -158,13 +173,11 @@ const ManualPanel = ({ form, labels }: ManualPanelProps): JSX.Element => ( - ); @@ -176,7 +189,8 @@ type BuildSamlPayloadParams = { manual?: { signOnUrl: string; issuer: string; - certFile: File | null; + certificates: IdpCertificateEntry[]; + initialCertificates: IdpCertificateEntry[]; }; }; @@ -213,8 +227,8 @@ export const buildSamlConfigurationPayload = async ({ idpEntityId: manual.issuer.trim(), }; - if (manual.certFile !== null) { - payload.idpCertificate = await manual.certFile.text(); + if (haveCertificatesChanged(manual.certificates, manual.initialCertificates)) { + payload.idpCertificates = toIdpCertificatesParam(manual.certificates); } return payload; @@ -350,3 +364,217 @@ const FileUploadField = ({ ); }; + +type CertificateListFieldProps = { + field: FormControl; + certificates: IdpCertificateEntry[]; + onCertificatesChange: React.Dispatch>; + labels: FileUploadLabels; +}; + +const CERTIFICATE_FILE_TYPES = '.pem,.key,.crt,.cer,.cert'; + +const CertificateListField = ({ + field, + certificates, + onCertificatesChange, + labels, +}: CertificateListFieldProps): JSX.Element => { + const { t } = useLocalizations(); + const inputRef = React.useRef(null); + const canRemove = certificates.length > 1; + const canAdd = certificates.length < MAX_IDP_CERTIFICATES; + + const onFileSelected = async (file: File | null): Promise => { + if (inputRef.current) { + inputRef.current.value = ''; + } + if (!file) { + return; + } + + let text: string; + try { + text = await file.text(); + } catch { + field.setError(t(localizationKeys('configureSSO.signingCertificates.fileUnreadable'))); + return; + } + + const bodies = parseCertificateFile(text); + if (!areCertificateBodies(bodies)) { + field.setError(t(localizationKeys('configureSSO.signingCertificates.notACertificate'))); + return; + } + + field.clearFeedback(); + onCertificatesChange(current => addCertificates(current, bodies)); + }; + + return ( + + + + + + + + void onFileSelected(e.target.files?.[0] ?? null)} + /> + + {certificates.length > 0 && ( + + {certificates.map((entry, index) => ( + ({ + padding: theme.space.$2, + borderRadius: theme.radii.$md, + borderWidth: theme.borderWidths.$normal, + borderStyle: theme.borderStyles.$solid, + borderColor: theme.colors.$borderAlpha100, + })} + > + + + + {entry.certificate} + + {index === 0 && ( + + + ({ display: 'inline-flex', borderRadius: theme.radii.$sm })} + > + + + + + + )} + + + + + + + ))} + + )} + + + + + + + ); +}; + +const CertificateExpiry = ({ entry }: { entry: IdpCertificateEntry }): JSX.Element => { + const status = getIdpCertificateStatus(entry); + const showsAlert = status === 'expired' || status === 'expiring'; + const colorScheme = status === 'expired' ? 'danger' : status === 'expiring' ? 'warning' : 'secondary'; + + return ( + + {showsAlert && ( + + )} + + + ); +}; diff --git a/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx b/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx index 44472584738..881522634ea 100644 --- a/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx +++ b/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx @@ -15,6 +15,11 @@ import { handleError } from '@/utils/errorHandler'; import type { LocalizationKey } from '../../../customizables'; import { Col, localizationKeys, Text } from '../../../customizables'; +import { + getIdpCertificateStatus, + type IdpCertificateEntry, + toIdpCertificateEntries, +} from '../../ConfigureSSO/domain/idpCertificates'; import { isOidcProvider } from '../../ConfigureSSO/domain/organizationEnterpriseConnection'; import type { EnterpriseConnectionMutations } from '../../ConfigureSSO/hooks/useOrganizationEnterpriseConnection'; import { @@ -40,7 +45,13 @@ type IdentityProviderSectionProps = { type FormScreenProps = IdentityProviderSectionProps & { onSuccess: () => void; onReset: () => void }; -type Detail = { id: string; label: LocalizationKey; value: string }; +type Detail = { + id: string; + label: LocalizationKey; + value?: string; + valueKey?: LocalizationKey; + tone?: 'danger' | 'warning'; +}; const SAML_MODES = ['metadataUrl', 'manual'] as const satisfies readonly SamlIdpConfigurationMode[]; const OIDC_MODES = ['discoveryUrl', 'manual'] as const satisfies readonly OidcIdpConfigurationMode[]; @@ -75,15 +86,56 @@ const samlDetails = (connection: EnterpriseConnectionResource): Detail[] => { }, ]; - if (saml && saml.idpCertificateExpiresAt > 0) { - details.push({ + const certificates = toIdpCertificateEntries(saml); + if (certificates.length > 0) { + details.push(certificatesDetail(certificates)); + } + + return details; +}; + +const toneFor = (entry: IdpCertificateEntry): Detail['tone'] => { + const status = getIdpCertificateStatus(entry); + return status === 'expired' ? 'danger' : status === 'expiring' ? 'warning' : undefined; +}; + +const certificatesDetail = (certificates: IdpCertificateEntry[]): Detail => { + const dated = certificates.filter(entry => entry.expiresAt !== null); + const earliest = dated.length > 0 ? dated.reduce((a, b) => ((b.expiresAt ?? 0) < (a.expiresAt ?? 0) ? b : a)) : null; + + if (certificates.length === 1) { + const [only] = certificates; + return { id: 'idpCertificateExpiresAt', label: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificateExpires'), - value: formatDate(new Date(saml.idpCertificateExpiresAt)), - }); + value: only.expiresAt === null ? undefined : formatDate(new Date(only.expiresAt)), + tone: toneFor(only), + }; } - return details; + const count = certificates.length; + if (!earliest || earliest.expiresAt === null) { + return { + id: 'idpCertificates', + label: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificates'), + valueKey: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificatesCount', { + count, + }), + }; + } + + const expired = getIdpCertificateStatus(earliest) === 'expired'; + return { + id: 'idpCertificates', + label: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificates'), + valueKey: localizationKeys( + expired + ? 'organizationProfile.securityPage.connectionPage.identityProvider.certificatesSummaryExpired' + : 'organizationProfile.securityPage.connectionPage.identityProvider.certificatesSummary', + { count, date: formatDate(new Date(earliest.expiresAt)) }, + ), + tone: toneFor(earliest), + }; }; const oidcDetails = (connection: EnterpriseConnectionResource): Detail[] => { @@ -138,7 +190,7 @@ export const IdentityProviderSection = (props: IdentityProviderSectionProps): JS ({ gap: t.space.$3, minWidth: 0 })}> {details - .filter(detail => detail.value) + .filter(detail => detail.value || detail.valueKey) .map(detail => ( - {detail.value} + + {detail.value} + ))} @@ -203,10 +261,10 @@ const SamlForm = withCardStateProvider( ({ connection, updateConnection, onSuccess, onReset }: FormScreenProps): JSX.Element => { const card = useCardState(); const saml = connection.samlConnection; - const existingCertPresent = Boolean(saml?.idpCertificate); + const initialCertificates = toIdpCertificateEntries(saml); const [mode, setMode] = useState(saml?.idpMetadataUrl ? 'metadataUrl' : 'manual'); - const [certFile, setCertFile] = useState(null); + const [certificates, setCertificates] = useState(initialCertificates); const metadataUrlField = useFormControl('idpMetadataUrl', saml?.idpMetadataUrl ?? '', { type: 'text', @@ -248,9 +306,7 @@ const SamlForm = withCardStateProvider( const isValid = mode === 'metadataUrl' ? metadataUrlField.value.trim().length > 0 - : signOnUrlField.value.trim().length > 0 && - issuerField.value.trim().length > 0 && - (certFile !== null || existingCertPresent); + : signOnUrlField.value.trim().length > 0 && issuerField.value.trim().length > 0 && certificates.length > 0; const formProps: IdentityProviderConfigurationFormProps = mode === 'metadataUrl' @@ -269,9 +325,9 @@ const SamlForm = withCardStateProvider( signOnUrlField, issuerField, certificateField, - certFile, - onCertFileChange: setCertFile, - existingCertPresent, + certificates, + onCertificatesChange: setCertificates, + initialCertificates, }, labels: { description: localizationKeys( @@ -305,7 +361,7 @@ const SamlForm = withCardStateProvider( const payload = await buildSamlConfigurationPayload({ mode, metadataUrl: { value: metadataUrlField.value }, - manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile }, + manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates }, }); await updateConnection(connection.id, { saml: payload }); diff --git a/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx b/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx index 004f1677179..d0295b1677e 100644 --- a/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx +++ b/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx @@ -10,6 +10,17 @@ import { EnterpriseConnectionPage } from '../EnterpriseConnectionPage'; const { createFixtures } = bindCreateFixtures('OrganizationProfile'); +// jsdom's File has no text(); the certificate upload reads the file with it. +if (typeof File.prototype.text !== 'function') { + File.prototype.text = function (this: File) { + return new Promise(resolve => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsText(this); + }); + }; +} + const withPageFixtures = (f: Parameters[0]>[0]) => { f.withEnterpriseSso({ selfServeSSO: true }); f.withEmailAddress(); @@ -206,6 +217,138 @@ describe('EnterpriseConnectionPage', () => { expect(await screen.findByText('Certificate expires')).toBeInTheDocument(); }); + it('lists every trusted certificate in the Edit form, marking the primary', async () => { + const { wrapper, fixtures } = await createFixtures(withPageFixtures); + withNoTestRuns(fixtures); + + const { userEvent, container } = renderPage( + wrapper, + fixtures, + samlConnectionWith({ + idpCertificates: [ + { certificate: 'CERTONE', issuedAt: null, expiresAt: Date.parse('2036-05-01T00:00:00Z') }, + { certificate: 'CERTTWO', issuedAt: null, expiresAt: Date.parse('2020-05-01T00:00:00Z') }, + ], + }), + ); + + await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2)); + await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]); + + const form = container.querySelector('.cl-actionCard') as HTMLElement; + expect(within(form).getByText('CERTONE')).toBeInTheDocument(); + expect(within(form).getByText('CERTTWO')).toBeInTheDocument(); + expect(within(form).getByText('Primary').closest('[tabindex="0"]')).not.toBeNull(); + expect(within(form).getByText(/^Expired /)).toBeInTheDocument(); + expect(within(form).getAllByRole('button', { name: 'Remove certificate' })).toHaveLength(2); + expect(within(form).getByRole('button', { name: 'Add certificate' })).toBeInTheDocument(); + }); + + it('adds the certificates of an uploaded bundle and saves the whole list', async () => { + const { wrapper, fixtures } = await createFixtures(withPageFixtures); + withNoTestRuns(fixtures); + fixtures.clerk.organization?.updateEnterpriseConnection.mockResolvedValue(samlConnection()); + + const { userEvent, container } = renderPage( + wrapper, + fixtures, + samlConnectionWith({ + idpCertificates: [{ certificate: 'CERTONE', issuedAt: null, expiresAt: Date.parse('2036-05-01T00:00:00Z') }], + }), + ); + + await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2)); + await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]); + + const form = container.querySelector('.cl-actionCard') as HTMLElement; + const bundle = new File( + [ + '-----BEGIN CERTIFICATE-----\nCERTONE\n-----END CERTIFICATE-----\n' + + '-----BEGIN CERTIFICATE-----\nCERT\nTWO\n-----END CERTIFICATE-----\n', + ], + 'bundle.pem', + ); + await userEvent.upload(form.querySelector('input[type="file"]') as HTMLInputElement, bundle); + + expect(await within(form).findByText('CERTTWO')).toBeInTheDocument(); + expect(within(form).getByText('Expiry shows after you save.')).toBeInTheDocument(); + + await userEvent.click(within(form).getByRole('button', { name: 'Save' })); + + await waitFor(() => { + expect(fixtures.clerk.organization?.updateEnterpriseConnection).toHaveBeenCalledWith('ent_1', { + saml: { + idpSsoUrl: 'https://idp.example.com/sso', + idpEntityId: 'https://idp.example.com/entity', + idpCertificates: ['CERTONE', 'CERTTWO'], + }, + }); + }); + }); + + it('removes a certificate but never the last one', async () => { + const { wrapper, fixtures } = await createFixtures(withPageFixtures); + withNoTestRuns(fixtures); + + const { userEvent, container } = renderPage( + wrapper, + fixtures, + samlConnectionWith({ + idpCertificates: [ + { certificate: 'CERTONE', issuedAt: null, expiresAt: null }, + { certificate: 'CERTTWO', issuedAt: null, expiresAt: null }, + ], + }), + ); + + await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2)); + await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]); + + const form = container.querySelector('.cl-actionCard') as HTMLElement; + await userEvent.click(within(form).getAllByRole('button', { name: 'Remove certificate' })[1]); + + expect(within(form).queryByText('CERTTWO')).not.toBeInTheDocument(); + expect(within(form).getByRole('button', { name: 'Remove certificate' })).toBeDisabled(); + }); + + it('rejects a file that is not a certificate', async () => { + const { wrapper, fixtures } = await createFixtures(withPageFixtures); + withNoTestRuns(fixtures); + + const { userEvent, container } = renderPage(wrapper, fixtures, samlConnection()); + + await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2)); + await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]); + + const form = container.querySelector('.cl-actionCard') as HTMLElement; + const key = new File(['-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n'], 'key.pem'); + await userEvent.upload(form.querySelector('input[type="file"]') as HTMLInputElement, key); + + expect(await within(form).findAllByText('One of the uploaded files is not a certificate.')).not.toHaveLength(0); + expect(within(form).getAllByRole('button', { name: 'Remove certificate' })).toHaveLength(1); + }); + + it('summarizes several certificates in the closed view', async () => { + const { wrapper, fixtures } = await createFixtures(withPageFixtures); + withNoTestRuns(fixtures); + + renderPage( + wrapper, + fixtures, + samlConnectionWith({ + idpCertificates: [ + { certificate: 'CERTONE', issuedAt: null, expiresAt: Date.parse('2036-05-01T00:00:00Z') }, + { certificate: 'CERTTWO', issuedAt: null, expiresAt: Date.parse('2030-05-01T00:00:00Z') }, + { certificate: 'CERTTHREE', issuedAt: null, expiresAt: null }, + ], + }), + ); + + expect(await screen.findByText('Certificates')).toBeInTheDocument(); + expect(screen.getByText('3 certificates, earliest expires May 1, 2030')).toBeInTheDocument(); + expect(screen.queryByText('Certificate expires')).not.toBeInTheDocument(); + }); + it('renders the OIDC variant of the service provider section and only the shared settings', async () => { const { wrapper, fixtures } = await createFixtures(withPageFixtures); withNoTestRuns(fixtures); diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts index bfad65bb647..e1ff3112d55 100644 --- a/packages/ui/src/customizables/elementDescriptors.ts +++ b/packages/ui/src/customizables/elementDescriptors.ts @@ -633,6 +633,12 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'configureSSOCertificateFileBadge', 'configureSSOCertificateFileName', 'configureSSOCertificateRemoveButton', + 'configureSSOCertificateList', + 'configureSSOCertificateListItem', + 'configureSSOCertificateListItemBody', + 'configureSSOCertificateListItemExpiry', + 'configureSSOCertificateListItemRemoveButton', + 'configureSSOCertificatePrimaryBadge', 'configureSSOTestUrlOpenButton', 'configureSSOTestRefreshButton', diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 5d1a89785ff..09fc4ffad1a 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -769,6 +769,12 @@ export type ElementsConfig = { configureSSOCertificateFileBadge: WithOptions; configureSSOCertificateFileName: WithOptions; configureSSOCertificateRemoveButton: WithOptions; + configureSSOCertificateList: WithOptions; + configureSSOCertificateListItem: WithOptions; + configureSSOCertificateListItemBody: WithOptions; + configureSSOCertificateListItemExpiry: WithOptions; + configureSSOCertificateListItemRemoveButton: WithOptions; + configureSSOCertificatePrimaryBadge: WithOptions; configureSSOTestUrlOpenButton: WithOptions; configureSSOTestRefreshButton: WithOptions;