Skip to content

Commit 4091449

Browse files
[2.10] Added input validation to s3 endpoint field in cluster (#15191)
1 parent c8e4cd0 commit 4091449

8 files changed

Lines changed: 183 additions & 18 deletions

File tree

cypress/e2e/tests/pages/manager/rke-templates.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('RKE Templates', { testIsolation: 'off', tags: ['@manager', '@adminUser
2626
rkeTemplatesPage.form().templateDetails(2).set(this.rkeRevisionName);
2727
cy.intercept('POST', '/v3/clustertemplate').as('createTemplate');
2828
rkeTemplatesPage.form().create();
29-
cy.wait('@createTemplate');
29+
cy.wait('@createTemplate').its('response.statusCode').should('eq', 201);
3030
rkeTemplatesPage.waitForPage();
3131
rkeTemplatesPage.groupRow().groupRowWithName(this.rkeTemplateName).should('be.visible');
3232
rkeTemplatesPage.groupRow().rowWithinGroupByName(this.rkeTemplateName, this.rkeRevisionName).should('be.visible');

shell/assets/translations/en-us.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,6 +2100,19 @@ cluster:
21002100
label: Metrics
21012101
false: Only available inside the cluster
21022102
true: Exposed to the public interface
2103+
s3config:
2104+
bucket:
2105+
label: Bucket
2106+
folder:
2107+
label: Folder
2108+
region:
2109+
label: Region
2110+
endpoint:
2111+
label: Endpoint
2112+
skipSSLVerify:
2113+
label: Accept any certificate (insecure)
2114+
endpointCA:
2115+
label: Endpoint CA Cert
21032116
k3s:
21042117
systemService:
21052118
coredns: 'CoreDNS'
@@ -6075,6 +6088,7 @@ validation:
60756088
localhost: If the Server URL is internal to the Rancher server (e.g. localhost) the downstream clusters may not be able to communicate with Rancher.
60766089
trailingForwardSlash: Server URL should not have a trailing forward slash.
60776090
url: Server URL must be an URL.
6091+
awsStyleEndpoint: Endpoint URL has to be a valid domain-based endpoint and cannot start with a protocol (e.g https:// or http://)
60786092
stringLength:
60796093
between: '"{key}" should be between {min} and {max} {max, plural, =1 {character} other {characters}}'
60806094
exactly: '"{key}" should be {count, plural, =1 {# character} other {# characters}}'

shell/edit/provisioning.cattle.io.cluster/rke2.vue

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ export default {
251251
addonConfigValidation: {}, // validation state of each addon config (boolean of whether codemirror's yaml lint passed)
252252
allNamespaces: [],
253253
extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.CLUSTER_CREATE_RKE2, this.$route, this),
254-
labelForAddon
254+
labelForAddon,
255+
etcdConfigValid: true
255256
};
256257
},
257258
@@ -831,7 +832,12 @@ export default {
831832
set(newValue) {
832833
this.$emit('update:value', newValue);
833834
}
834-
}
835+
},
836+
overallFormValidationPassed() {
837+
return this.validationPassed &&
838+
this.fvFormIsValid &&
839+
this.etcdConfigValid;
840+
},
835841
},
836842
837843
watch: {
@@ -2175,7 +2181,7 @@ export default {
21752181
v-else
21762182
ref="cruresource"
21772183
:mode="mode"
2178-
:validation-passed="validationPassed && fvFormIsValid"
2184+
:validation-passed="overallFormValidationPassed"
21792185
:resource="value"
21802186
:errors="errors"
21812187
:cancel-event="true"
@@ -2396,6 +2402,7 @@ export default {
23962402
@update:value="$emit('input', $event)"
23972403
@s3-backup-changed="handleS3BackupChanged"
23982404
@config-etcd-expose-metrics-changed="handleConfigEtcdExposeMetricsChanged"
2405+
@etcd-validation-changed="(val)=>etcdConfigValid = val"
23992406
/>
24002407
</Tab>
24012408

shell/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { Checkbox } from '@components/Form/Checkbox';
33
import { LabeledInput } from '@components/Form/LabeledInput';
44
import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret';
55
import { NORMAN } from '@shell/config/types';
6+
import FormValidation from '@shell/mixins/form-validation';
67
78
export default {
8-
emits: ['update:value'],
9+
emits: ['update:value', 'validationChanged'],
910
1011
components: {
1112
LabeledInput,
1213
Checkbox,
1314
SelectOrCreateAuthSecret,
1415
},
15-
16-
props: {
16+
mixins: [FormValidation],
17+
props: {
1718
mode: {
1819
type: String,
1920
required: true,
@@ -48,7 +49,17 @@ export default {
4849
...(this.value || {}),
4950
};
5051
51-
return { config };
52+
return {
53+
config,
54+
fvFormRuleSets: [
55+
{
56+
path: 'endpoint', rootObject: this.config, rules: ['awsStyleEndpoint']
57+
},
58+
{
59+
path: 'bucket', rootObject: this.config, rules: ['required']
60+
},
61+
]
62+
};
5263
},
5364
5465
computed: {
@@ -64,6 +75,11 @@ export default {
6475
return {};
6576
},
6677
},
78+
watch: {
79+
fvFormIsValid(newValue) {
80+
this.$emit('validationChanged', !!newValue);
81+
}
82+
},
6783
6884
methods: {
6985
update() {
@@ -79,6 +95,7 @@ export default {
7995
<div>
8096
<SelectOrCreateAuthSecret
8197
v-model:value="config.cloudCredentialName"
98+
:mode="mode"
8299
:register-before-hook="registerBeforeHook"
83100
in-store="management"
84101
:allow-ssh="false"
@@ -94,7 +111,8 @@ export default {
94111
<div class="col span-6">
95112
<LabeledInput
96113
v-model:value="config.bucket"
97-
label="Bucket"
114+
:label="t('cluster.rke2.etcd.s3config.bucket.label')"
115+
:mode="mode"
98116
:placeholder="ccData.defaultBucket"
99117
:required="!ccData.defaultBucket"
100118
@update:value="update"
@@ -103,7 +121,8 @@ export default {
103121
<div class="col span-6">
104122
<LabeledInput
105123
v-model:value="config.folder"
106-
label="Folder"
124+
:label="t('cluster.rke2.etcd.s3config.folder.label')"
125+
:mode="mode"
107126
:placeholder="ccData.defaultFolder"
108127
@update:value="update"
109128
/>
@@ -114,16 +133,19 @@ export default {
114133
<div class="col span-6">
115134
<LabeledInput
116135
v-model:value="config.region"
117-
label="Region"
136+
:label="t('cluster.rke2.etcd.s3config.region.label')"
137+
:mode="mode"
118138
:placeholder="ccData.defaultRegion"
119139
@update:value="update"
120140
/>
121141
</div>
122142
<div class="col span-6">
123143
<LabeledInput
124144
v-model:value="config.endpoint"
125-
label="Endpoint"
145+
:label="t('cluster.rke2.etcd.s3config.endpoint.label')"
146+
:mode="mode"
126147
:placeholder="ccData.defaultEndpoint"
148+
:rules="fvGetAndReportPathRules('endpoint')"
127149
@update:value="update"
128150
/>
129151
</div>
@@ -136,15 +158,15 @@ export default {
136158
<Checkbox
137159
v-model:value="config.skipSSLVerify"
138160
:mode="mode"
139-
label="Accept any certificate (insecure)"
161+
:label="t('cluster.rke2.etcd.s3config.skipSSLVerify.label')"
140162
@update:value="update"
141163
/>
142164

143165
<LabeledInput
144166
v-if="!config.skipSSLVerify"
145167
v-model:value="config.endpointCA"
146168
type="multiline"
147-
label="Endpoint CA Cert"
169+
:label="t('cluster.rke2.etcd.s3config.endpointCA.label')"
148170
:placeholder="ccData.defaultEndpointCA"
149171
@update:value="update"
150172
/>

shell/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import S3Config from '@shell/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Con
77
import UnitInput from '@shell/components/form/UnitInput';
88
99
export default {
10-
emits: ['s3-backup-changed', 'config-etcd-expose-metrics-changed'],
10+
emits: ['s3-backup-changed', 'config-etcd-expose-metrics-changed', 'etcd-validation-changed'],
1111
1212
components: {
1313
LabeledInput,
@@ -55,6 +55,18 @@ export default {
5555
},
5656
5757
},
58+
methods: {
59+
s3BackupChanged(val) {
60+
this.$emit('s3-backup-changed', val);
61+
this.$emit('etcd-validation-changed', !val);
62+
},
63+
disableSnapshotsChanged(val) {
64+
if (val) {
65+
this.$emit('etcd-validation-changed', true);
66+
this.$emit('s3-backup-changed', false);
67+
}
68+
}
69+
}
5870
};
5971
</script>
6072
@@ -69,6 +81,7 @@ export default {
6981
:label="t('cluster.rke2.etcd.disableSnapshots.label')"
7082
:labels="[t('generic.disable'), t('generic.enable')]"
7183
:mode="mode"
84+
@update:value="disableSnapshotsChanged"
7285
/>
7386
</div>
7487
</div>
@@ -105,7 +118,7 @@ export default {
105118
label="Backup Snapshots to S3"
106119
:labels="['Disable','Enable']"
107120
:mode="mode"
108-
@update:value="$emit('s3-backup-changed', $event)"
121+
@update:value="s3BackupChanged"
109122
/>
110123
111124
<S3Config
@@ -114,6 +127,7 @@ export default {
114127
:namespace="value.metadata.namespace"
115128
:register-before-hook="registerBeforeHook"
116129
:mode="mode"
130+
@validationChanged="$emit('etcd-validation-changed', $event)"
117131
/>
118132
</template>
119133
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
isServerUrl,
3+
isHttps,
4+
isDomainWithoutProtocol,
5+
isLocalhost,
6+
hasTrailingForwardSlash,
7+
} from '@shell/utils/validators/setting';
8+
9+
describe('isServerUrl', () => {
10+
it.each([
11+
['server-url', true],
12+
['SERVER-URL', false],
13+
['server-url/', false],
14+
['not-server-url', false],
15+
['', false],
16+
])('should validate that isServerUrl("%s") returns %s', (input, expected) => {
17+
expect(isServerUrl(input)).toBe(expected);
18+
});
19+
});
20+
21+
describe('isHttps', () => {
22+
it.each([
23+
['https://example.com', true],
24+
['HTTPS://EXAMPLE.COM', true],
25+
['http://example.com', false],
26+
['ftp://example.com', false],
27+
['example.com', false],
28+
['', false],
29+
])('should validate that isHttps("%s") returns %s', (input, expected) => {
30+
expect(isHttps(input)).toBe(expected);
31+
});
32+
});
33+
34+
describe('isDomainWithoutProtocol (follows domain format, no protocol)', () => {
35+
it.each([
36+
['ec2.us-west-2.amazonaws.com', true],
37+
['ec2.us-west-2.api.aws', true],
38+
['ec2.us-west-2.amazonaws.com.cn', true],
39+
['s3.eu-central-1.amazonaws.com.cn', true],
40+
['my-service.internal.net', true],
41+
['db.example.org:5432', true],
42+
['ex.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.com', true],
43+
['service.company.local/path/to/resource', true],
44+
['example.org:443', true],
45+
['https://ec2.us-west-2.amazonaws.com', false],
46+
['http://example.com', false],
47+
['udp://example.com', false],
48+
['ftp://example.com', false],
49+
['ftps://example.com', false],
50+
['example://example.com', false],
51+
['example', false],
52+
['-bad.example.com', false],
53+
['bad-.example.com', false],
54+
['example.c', false],
55+
['exa mple.com', false],
56+
['exa.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.com', false],
57+
['', false],
58+
])('should validate that isDomainWithoutProtocol("%s") returns %s', (input, expected) => {
59+
expect(isDomainWithoutProtocol(input)).toBe(expected);
60+
});
61+
});
62+
63+
describe('isLocalhost', () => {
64+
it.each([
65+
['localhost', true],
66+
['LOCALHOST', true],
67+
['http://localhost', true],
68+
['https://localhost:3000', true],
69+
['127.0.0.1', true],
70+
['http://127.0.0.1:8080/path', true],
71+
['HTTPS://127.0.0.1/Health', true],
72+
['127.0.0.2', false],
73+
['http://127.0.0.2', false],
74+
['mylocalhost', false],
75+
])('should validate that isLocalhost("%s") returns %s', (input, expected) => {
76+
expect(isLocalhost(input)).toBe(expected);
77+
});
78+
});
79+
80+
describe('hasTrailingForwardSlash', () => {
81+
it.each([
82+
['https://example.com/', true],
83+
['http://example.com/', true],
84+
['HTTPS://EXAMPLE.COM/', true],
85+
['https://example.com/path/', true],
86+
['https://example.com', false],
87+
['http://example.com/path', false],
88+
['example.com/', false],
89+
])('should validate that hasTrailingForwardSlash("%s") returns %s', (input, expected) => {
90+
expect(hasTrailingForwardSlash(input)).toBe(expected);
91+
});
92+
});

shell/utils/validators/formRules/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import isUrl from 'is-url';
66
// import uniq from 'lodash/uniq';
77
import cronstrue from 'cronstrue';
88
import { Translation } from '@shell/types/t';
9-
import { isHttps, isLocalhost, hasTrailingForwardSlash } from '@shell/utils/validators/setting';
9+
import { isHttps, isLocalhost, hasTrailingForwardSlash, isDomainWithoutProtocol } from '@shell/utils/validators/setting';
1010

1111
// import uniq from 'lodash/uniq';
1212
export type Validator<T = undefined | string> = (val: any, arg?: any) => T;
@@ -136,7 +136,7 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
136136
return t('validation.invalidCron');
137137
}
138138
};
139-
139+
const awsStyleEndpoint: Validator = (val: string) => val && !isDomainWithoutProtocol(val) ? t('validation.setting.serverUrl.awsStyleEndpoint') : undefined;
140140
const https: Validator = (val: string) => val && !isHttps(val) ? t('validation.setting.serverUrl.https') : undefined;
141141

142142
const localhost: Validator = (val: string) => isLocalhost(val) ? t('validation.setting.serverUrl.localhost') : undefined;
@@ -478,6 +478,7 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
478478
hostname,
479479
imageUrl,
480480
interval,
481+
awsStyleEndpoint,
481482
https,
482483
localhost,
483484
trailingForwardSlash,

shell/utils/validators/setting.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import isUrl from 'is-url';
22

3+
// Note that these function cover specific use cases and you need to make sure it works for your use case before using them.
4+
// ie they would consider empty values as valid, not all endpoint formatting is enforced
35
export const isServerUrl = (value) => value === 'server-url';
46

57
export const isHttps = (value) => value.toLowerCase().startsWith('https://');
68

79
export const isLocalhost = (value) => (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)/i).test(value);
810

911
export const hasTrailingForwardSlash = (value) => isUrl(value) && value?.toLowerCase().endsWith('/');
12+
/**
13+
* Checks that provided string is a domain without protocol (case insensitive):
14+
* - Cannot start with any protocol, such as http://, https://, ftp://, ftps://, udp://
15+
* - Must only use the letters a to z, the numbers 0 to 9, and the dot (.) and hyphen (-) characters.
16+
* - if the hyphen character is used in a domain name, it cannot be the first or the last character in the name.
17+
* - The length of each label can be 2-63 characters
18+
* - TLD is at least 2 characters
19+
* - The total length of a domain name, including the dot at the end, cannot exceed 254 characters.
20+
* - Allows for optional port and path
21+
* @param {*} value
22+
* @returns boolean indicating if the value is a domain without protocol
23+
*/
24+
export const isDomainWithoutProtocol = (value) => (/^(?=.{1,254}$)(?![a-z][a-z0-9+.-]*:\/\/)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{1,5})?(?:\/\S*)?$/i).test(value);

0 commit comments

Comments
 (0)