From 15b7dad8f534277b3a32453055993a1064b17c70 Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Tue, 3 Mar 2026 11:23:48 +0100 Subject: [PATCH] EDM-3390: Validate certificate fields in repositories --- libs/i18n/locales/en/translation.json | 8 +- .../Repository/CreateRepository/utils.ts | 83 +++++++++++++------ 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 04ba5a946..6cd32055c 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -1149,13 +1149,15 @@ "Git repository": "Git repository", "Target revision is required.": "Target revision is required.", "Must be an absolute path.": "Must be an absolute path.", - "Repository type is required": "Repository type is required", + "Value must not exceed {{ maxSize }} MiB when encoded.": "Value must not exceed {{ maxSize }} MiB when encoded.", + "Invalid format. Only ASCII characters are allowed.": "Invalid format. Only ASCII characters are allowed.", + "Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)": "Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)", + "Registry hostname is required": "Registry hostname is required", "Password is required": "Password is required", + "Repository type is required": "Repository type is required", "Client TLS certificate is required": "Client TLS certificate is required", "Client TLS key is required": "Client TLS key is required", "Must be a valid JWT token": "Must be a valid JWT token", - "Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)": "Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)", - "Registry hostname is required": "Registry hostname is required", "Enter a valid repository URL. Example: {{ demoRepositoryUrl }}": "Enter a valid repository URL. Example: {{ demoRepositoryUrl }}", "Repository URL is required": "Repository URL is required", "Enter a valid HTTP service URL. Example: https://my-service-url": "Enter a valid HTTP service URL. Example: https://my-service-url", diff --git a/libs/ui-components/src/components/Repository/CreateRepository/utils.ts b/libs/ui-components/src/components/Repository/CreateRepository/utils.ts index 8a0500d98..c3f194589 100644 --- a/libs/ui-components/src/components/Repository/CreateRepository/utils.ts +++ b/libs/ui-components/src/components/Repository/CreateRepository/utils.ts @@ -29,6 +29,9 @@ const httpRepoUrlRegex = /^(http|https)/; const pathRegex = /\/.+/; const jwtTokenRegexp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; +const MAX_REPO_ENCODED_CERT_LENGTH = 20 * 1024 * 1024; // 20 MiB as per the backend +const MAX_REPO_ORIGINAL_CERT_LENGTH = Math.floor((MAX_REPO_ENCODED_CERT_LENGTH / 4) * 3); // 15 MiB + export const isHttpRepoSpec = (repoSpec: RepositorySpec): repoSpec is HttpRepoSpec => repoSpec.type === RepoSpecType.RepoSpecTypeHttp; export const isGitRepoSpec = (repoSpec: RepositorySpec): repoSpec is GitRepoSpec => @@ -680,38 +683,36 @@ export const singleResourceSyncSchema = (t: TFunction, existingRSs: ResourceSync }); }; +const validRepositoryCertificate = (t: TFunction) => (value: string | undefined, testContext: Yup.TestContext) => { + if (!value || value.trim() === '') { + return true; + } + if (value.length > MAX_REPO_ORIGINAL_CERT_LENGTH) { + return testContext.createError({ + message: t('Value must not exceed {{ maxSize }} MiB when encoded.', { + maxSize: MAX_REPO_ENCODED_CERT_LENGTH / 1024 / 1024, + }), + }); + } + try { + btoa(value); + return true; + } catch { + return testContext.createError({ + message: t('Invalid format. Only ASCII characters are allowed.'), + }); + } +}; + // Regex for registry hostname: FQDN, IP address (IPv4 or IPv6), with optional port, matching as much as possible of the backend pattern const registryHostnameRegex = /^(([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)*[a-z]([-a-z0-9]*[a-z0-9])?|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\[[a-fA-F0-9:]+\])(:[0-9]{1,5})?$/; export const repositorySchema = (t: TFunction, repository: Repository | undefined) => (values: RepositoryFormValues) => { - const baseSchema = { - name: validKubernetesDnsSubdomain(t, { isRequired: !repository }), - configType: values.useAdvancedConfig ? Yup.string().required(t('Repository type is required')) : Yup.string(), - httpConfig: Yup.object({ - basicAuth: Yup.object({ - username: values.httpConfig?.basicAuth?.use ? Yup.string().required(t('Username is required')) : Yup.string(), - password: values.httpConfig?.basicAuth?.use ? Yup.string().required(t('Password is required')) : Yup.string(), - }), - mTlsAuth: Yup.object({ - tlsCrt: values.httpConfig?.mTlsAuth?.use - ? Yup.string().required(t('Client TLS certificate is required')) - : Yup.string(), - tlsKey: values.httpConfig?.mTlsAuth?.use - ? Yup.string().required(t('Client TLS key is required')) - : Yup.string(), - }), - token: Yup.string().matches(jwtTokenRegexp, t('Must be a valid JWT token')), - }), - useResourceSyncs: Yup.boolean(), - resourceSyncs: values.useResourceSyncs ? repoSyncSchema(t, values.resourceSyncs) : Yup.array(), - }; - if (values.repoType === RepoSpecType.RepoSpecTypeOci) { return Yup.object({ - ...baseSchema, - url: Yup.string(), + name: validKubernetesDnsSubdomain(t, { isRequired: !repository }), ociConfig: Yup.object({ registry: Yup.string() .matches( @@ -726,14 +727,43 @@ export const repositorySchema = username: values.ociConfig?.ociAuth?.use ? Yup.string().required(t('Username is required')) : Yup.string(), password: values.ociConfig?.ociAuth?.use ? Yup.string().required(t('Password is required')) : Yup.string(), }), - caCrt: Yup.string(), + caCrt: Yup.string().test('valid-certificate', validRepositoryCertificate(t)), skipServerVerification: Yup.boolean(), }), }); } + // Git or Http repositories return Yup.object({ - ...baseSchema, + name: validKubernetesDnsSubdomain(t, { isRequired: !repository }), + configType: values.useAdvancedConfig ? Yup.string().required(t('Repository type is required')) : Yup.string(), + httpConfig: Yup.object({ + basicAuth: Yup.object({ + username: values.httpConfig?.basicAuth?.use ? Yup.string().required(t('Username is required')) : Yup.string(), + password: values.httpConfig?.basicAuth?.use ? Yup.string().required(t('Password is required')) : Yup.string(), + }), + caCrt: Yup.string().test('valid-certificate', validRepositoryCertificate(t)), + mTlsAuth: Yup.object({ + tlsCrt: values.httpConfig?.mTlsAuth?.use + ? Yup.string() + .required(t('Client TLS certificate is required')) + .test('valid-certificate', validRepositoryCertificate(t)) + : Yup.string(), + tlsKey: values.httpConfig?.mTlsAuth?.use + ? Yup.string() + .required(t('Client TLS key is required')) + .test('valid-certificate', validRepositoryCertificate(t)) + : Yup.string(), + }), + token: Yup.string().matches(jwtTokenRegexp, t('Must be a valid JWT token')), + }), + sshConfig: Yup.object({ + sshPrivateKey: Yup.string().test('valid-certificate', validRepositoryCertificate(t)), + privateKeyPassphrase: Yup.string(), + skipServerVerification: Yup.boolean(), + }), + useResourceSyncs: Yup.boolean(), + resourceSyncs: values.useResourceSyncs ? repoSyncSchema(t, values.resourceSyncs) : Yup.array(), url: Yup.string().when('repoType', { is: (repoType: RepoSpecType) => repoType === RepoSpecType.RepoSpecTypeGit, then: () => @@ -750,7 +780,6 @@ export const repositorySchema = .matches(httpRepoUrlRegex, t('Enter a valid HTTP service URL. Example: https://my-service-url')) .defined(t('HTTP service URL is required')), }), - ociConfig: Yup.object(), }); };