diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index 1b1eefdbf1..ca5a6eef7d 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -6761,10 +6761,10 @@ validation: flowOutput: both: Requires "Output" or "Cluster Output" to be selected. global: Requires "Cluster Output" to be selected. - git: - url: URL must be a HTTP(s) or SSH url with no trailing spaces + repository: + url: It must be a valid HTTP(s) or SSH URL with no trailing spaces oci: - url: URL must be an OCI url with no trailing spaces + url: It must be a valid OCI URL with no trailing spaces output: logdna: apiKey: Required an "Api Key" to be set. diff --git a/shell/edit/fleet.cattle.io.gitrepo.vue b/shell/edit/fleet.cattle.io.gitrepo.vue index 7c884e74bf..3b4ad44645 100644 --- a/shell/edit/fleet.cattle.io.gitrepo.vue +++ b/shell/edit/fleet.cattle.io.gitrepo.vue @@ -107,10 +107,7 @@ export default { targetsCreated: '', fvFormRuleSets: [{ path: 'spec.repo', - rules: [ - 'required', - 'urlRepository' - ], + rules: ['urlRepository'], }], touched: null, }; diff --git a/shell/edit/fleet.cattle.io.helmop.vue b/shell/edit/fleet.cattle.io.helmop.vue index 3e1b7aca88..bbcceeeb24 100644 --- a/shell/edit/fleet.cattle.io.helmop.vue +++ b/shell/edit/fleet.cattle.io.helmop.vue @@ -404,7 +404,7 @@ export default { case SOURCE_TYPE.REPO: this.fvFormRuleSets = [{ path: 'spec.helm.repo', - rules: ['required', 'urlRepository'], + rules: ['urlRepository'], }, { path: 'spec.helm.chart', rules: ['required'], @@ -416,7 +416,7 @@ export default { case SOURCE_TYPE.OCI: this.fvFormRuleSets = [{ path: 'spec.helm.repo', - rules: ['required', 'ociRegistry'], + rules: ['ociRegistry'], }, { path: 'spec.helm.version', rules: ['semanticVersion'], @@ -425,7 +425,7 @@ export default { case SOURCE_TYPE.TARBALL: this.fvFormRuleSets = [{ path: 'spec.helm.chart', - rules: ['required', 'urlRepository'], + rules: ['urlRepository'], }]; break; } diff --git a/shell/utils/validators/formRules/__tests__/index.test.ts b/shell/utils/validators/formRules/__tests__/index.test.ts index 61f313c5df..3ca3f9122d 100644 --- a/shell/utils/validators/formRules/__tests__/index.test.ts +++ b/shell/utils/validators/formRules/__tests__/index.test.ts @@ -97,32 +97,54 @@ describe('formRules', () => { }); describe('urlRepository', () => { - const message = JSON.stringify({ message: 'validation.git.url' }); + const message = JSON.stringify({ message: 'validation.repository.url' }); const testCases = [ // Valid HTTP(s) ['https://github.com/rancher/dashboard.git', undefined], ['http://github.com/rancher/dashboard.git', undefined], ['https://github.com/rancher/dashboard', undefined], ['https://github.com/rancher/dashboard/', undefined], + ['https://github.com/rancher/%20dashboard/', undefined], + ['https://github.com/rancher/dashboard/%20', undefined], + ['https://localhost:8005', undefined], // Valid SSH ['git@github.com:rancher/dashboard.git', undefined], ['git@github.com:rancher/dashboard', undefined], ['git@github.com:rancher/dashboard/', undefined], + ['git@github.com:rancher/%20dashboard/', undefined], + ['git@github.com:rancher/dashboard/%20', undefined], // Not valid HTTP(s) ['https://github.com/rancher/ dashboard.git', message], ['http://github.com/rancher/ dashboard.git', message], + ['http://github.com/ rancher/dashboard.git', message], + ['http://github.com /rancher/dashboard.git', message], ['https://github.com/rancher/dashboard ', message], + ['https%20://github.com/rancher/dashboard ', message], + ['ht%20tps://github.com/rancher/dashboard ', message], + ['https://git%20hub.com/rancher/dashboard/%20', message], + ['https://https://', message], + ['http:/ww.abc.com', message], + ['http:ww.abc.com', message], ['foo://github.com/rancher/dashboard/', message], ['github.com/rancher/dashboard/', message], // Not valid SSH ['git@github.com:rancher/ dashboard.git', message], ['git@github.com:rancher/dashboard ', message], + ['git@github.com:rancher/ dashboard', message], + ['git @github.com:rancher/dashboard', message], + ['git@github.com: rancher/dashboard', message], ['git@github.comrancher/dashboard', message], + ['git@githubcomrancher/dashboard', message], + ['%20git@github.comrancher/dashboard', message], + ['git@git%20hub.comrancher/dashboard', message], + ['git@.git', message], + ['git@', message], - [undefined, undefined] + [undefined, message], + ['', message] ]; it.each(testCases)( @@ -139,20 +161,26 @@ describe('formRules', () => { const message = JSON.stringify({ message: 'validation.oci.url' }); const testCases = [ // Valid - ['oci://bucket/object', undefined], + ['oci://registry.example.com', undefined], + ['oci://myregistry.dev:5000', undefined], + ['oci://192.168.1.100', undefined], + ['oci://my.domain.com/my/image:tag', undefined], + ['oci://localhost:5000', undefined], ['oci://region.objectstorage.example.com/n', undefined], - ['oci://a', undefined], - ['oci://UPPERCASE/path', undefined], // Invalid ['http://example.com/oci', message], ['https://oci.cloud.com', message], ['ftp://oci.server.net', message], - ['/path/to/oci', message], + ['path/to/oci', message], + ['oci://a', message], ['oci:/missing/slash', message], ['oci:', message], ['oci://', message], - ['oci://space between', message], + ['oci://oci://duplicate/protocol', message], + ['oci ://registry.example.com/foo/bar', message], + ['oci://registry.example. com/foo/bar', message], + ['oci://registry.example.com/ foo/bar', message], ['oci://resource multiple spaces', message], ['', message], [undefined, message], diff --git a/shell/utils/validators/formRules/index.ts b/shell/utils/validators/formRules/index.ts index 80dc0caf54..0dbb36b2c6 100644 --- a/shell/utils/validators/formRules/index.ts +++ b/shell/utils/validators/formRules/index.ts @@ -1,4 +1,5 @@ import semver from 'semver'; +import { parse } from '@shell/utils/url'; import { RBAC } from '@shell/config/types'; import { HCI } from '@shell/config/labels-annotations'; import isEmpty from 'lodash/isEmpty'; @@ -174,22 +175,82 @@ export default function( const genericUrl: Validator = (val: string) => val && !isUrl(val) ? t('validation.genericUrl') : undefined; const urlRepository: Validator = (url: string) => { - const regexPart1 = /^((http|git|ssh|http(s)|file|\/?)|(git@[\w\.]+))(:(\/\/)?)/gm; - const regexPart2 = /^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(.git){0,1}(\/)?$/gm; + const message = t('validation.repository.url'); - if (url) { - const urlPart2 = url.replaceAll(regexPart1, ''); + if (!url) { + return message; + } - return !urlPart2 || url === urlPart2 || !regexPart2.test(urlPart2.replaceAll('%20', '')) ? t('validation.git.url') : undefined; + if (url.includes(' ')) { + return message; + } + + const { + protocol, + authority, + host, + path + } = parse(url); + + // Test duplicate protocol + if (!host || protocol === host) { + return message; + } + + // Test http(s) protocol + if (protocol && (!/^(http|http(s))/gm.test(protocol) || (!url.startsWith('https://') && !url.startsWith('http://')))) { + return message; + } + + // Test ssh, authority must be valid (SSH user + host) + if (!protocol && !authority.endsWith(':')) { + return message; + } + + // Encoded space characters (%20) are allowed only in the path + const hostAndPath = `${ host }${ path.replaceAll('%20', '') }`; + + // Test host/path + if (!/^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(.git){0,1}(\/)?$/gm.test(hostAndPath)) { + return message; } return undefined; }; const ociRegistry: Validator = (url: string) => { - const regex = /^oci:\/\/\S+$/gm; + const message = t('validation.oci.url'); - return !regex.test(url) ? t('validation.oci.url') : undefined; + if (!url) { + return message; + } + + if (url.includes(' ')) { + return message; + } + + const { + protocol, + host, + path + } = parse(url); + + // Test duplicate protocol + if (!host || protocol === host) { + return message; + } + + // Test oci protocol + if (!url.startsWith('oci://')) { + return message; + } + + // Test host/path + if (!/^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(\/)?$/gm.test(`${ host }${ path }`)) { + return message; + } + + return undefined; }; const version: Validator = (value: string) => {