Validating server url on Setup & Global Settings pages (#9701)

* added more validations for server-url on Setup and Global Settings pages

* handling server-url 'use default value' case

* consistent error messages

---------

Co-authored-by: Mo Mesgin <mmesgin@Mos-M2-MacBook-Pro.local>
This commit is contained in:
momesgin 2023-10-26 11:17:45 -07:00 committed by GitHub
parent 5cf644c86e
commit 5919915b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 157 additions and 36 deletions

View File

@ -142,6 +142,7 @@
"@nuxtjs/eslint-config-typescript": "6.0.1",
"@types/copy-webpack-plugin": "^5.0.3",
"@types/dompurify": "3.0.0",
"@types/is-url": "1.2.30",
"@types/jest": "27.4.1",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "4.14.184",

View File

@ -5755,7 +5755,10 @@ validation:
required: 'Port Rule [{position}] - Target Port is required'
setting:
serverUrl:
https: server-url must be https.
https: Server URL must be https.
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.
trailingForwardSlash: Server URL should not have a trailing forward slash.
url: Server URL must be an URL.
stringLength:
between: '"{key}" should be between {min} and {max} {max, plural, =1 {character} other {characters}}'
exactly: '"{key}" should be {count, plural, =1 {# character} other {# characters}}'

View File

@ -2,6 +2,7 @@
import CruResource from '@shell/components/CruResource';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner';
import CreateEditView from '@shell/mixins/create-edit-view';
import { TextAreaAutoGrow } from '@components/Form/TextArea';
import formRulesGenerator from '@shell/utils/validators/formRules/index';
@ -11,6 +12,7 @@ import { RadioGroup } from '@components/Form/Radio';
import FormValidation from '@shell/mixins/form-validation';
import { setBrand } from '@shell/config/private-label';
import { keyBy, mapValues } from 'lodash';
import { isLocalhost, isServerUrl } from '@shell/utils/validators/setting';
export default {
components: {
@ -18,7 +20,8 @@ export default {
LabeledInput,
LabeledSelect,
RadioGroup,
TextAreaAutoGrow
TextAreaAutoGrow,
Banner
},
mixins: [CreateEditView, FormValidation],
@ -63,6 +66,14 @@ export default {
return factoryArg ? rule(factoryArg) : rule;
}) : {};
},
showLocalhostWarning() {
return isServerUrl(this.value.id) && isLocalhost(this.value.value);
},
validationPassed() {
return this.fvFormIsValid && this.fvGetPathErrors(['value']).length === 0;
}
},
@ -98,6 +109,11 @@ export default {
if (ev && ev.srcElement) {
ev.srcElement.blur();
}
if (isServerUrl(this.value.id) && !this.value.default) {
return;
}
this.value.value = this.value.default;
}
}
@ -113,7 +129,7 @@ export default {
:resource="value"
:subtypes="[]"
:can-yaml="false"
:validation-passed="fvFormIsValid"
:validation-passed="validationPassed"
@error="e=>errors = e"
@finish="saveSettings"
@cancel="done"
@ -138,6 +154,19 @@ export default {
</button>
</div>
<Banner
v-if="showLocalhostWarning"
color="warning"
:label="t('validation.setting.serverUrl.localhost')"
/>
<Banner
v-for="(err, i) in fvGetPathErrors(['value'])"
:key="i"
color="error"
:label="err"
/>
<div class="mt-20">
<div v-if="setting.kind === 'enum'">
<LabeledSelect

View File

@ -1,5 +1,6 @@
import { ALLOWED_SETTINGS } from '@shell/config/settings';
import HybridModel from '@shell/plugins/steve/hybrid-class';
import { isServerUrl } from '@shell/utils/validators/setting';
export default class Setting extends HybridModel {
get fromEnv() {
@ -31,12 +32,15 @@ export default class Setting extends HybridModel {
}
get customValidationRules() {
return [
{
path: 'value',
translationKey: 'setting.serverUrl.https',
validators: [`isHttps:${ this.metadata.name }`]
},
];
const out = [];
if (isServerUrl(this.metadata.name)) {
out.push({
path: 'value',
validators: ['required', 'https', 'url', 'trailingForwardSlash']
});
}
return out;
}
}

View File

@ -18,6 +18,10 @@ import Password from '@shell/components/form/Password';
import { applyProducts } from '@shell/store/type-map';
import BrandImage from '@shell/components/BrandImage';
import { waitFor } from '@shell/utils/async';
import { Banner } from '@components/Banner';
import FormValidation from '@shell/mixins/form-validation';
import isUrl from 'is-url';
import { isLocalhost } from '@shell/utils/validators/setting';
const calcIsFirstLogin = (store) => {
const firstLoginSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
@ -36,11 +40,18 @@ const calcMustChangePassword = async(store) => {
export default {
layout: 'unauthenticated',
mixins: [FormValidation],
data() {
return {
passwordOptions: [
{ label: this.t('setup.useRandom'), value: true },
{ label: this.t('setup.useManual'), value: false }],
fvFormRuleSets: [{
path: 'serverUrl',
rootObject: this,
rules: ['required', 'https', 'url', 'trailingForwardSlash']
}]
};
},
@ -78,7 +89,7 @@ export default {
},
components: {
AsyncButton, LabeledInput, CopyToClipboard, Checkbox, RadioGroup, Password, BrandImage
AsyncButton, LabeledInput, CopyToClipboard, Checkbox, RadioGroup, Password, BrandImage, Banner
},
async asyncData({ route, req, store }) {
@ -191,6 +202,10 @@ export default {
}
}
if (!isUrl(this.serverUrl) || this.fvGetPathErrors(['serverUrl']).length > 0) {
return false;
}
return true;
},
@ -198,6 +213,10 @@ export default {
const out = findBy(this.principals, 'me', true);
return out;
},
showLocalhostWarning() {
return isLocalhost(this.serverUrl);
}
},
@ -265,6 +284,10 @@ export default {
done() {
this.$router.replace('/');
},
onServerUrlChange(value) {
this.serverUrl = value.trim();
},
},
};
</script>
@ -368,10 +391,24 @@ export default {
/>
</p>
<div class="mt-20">
<Banner
v-if="showLocalhostWarning"
color="warning"
:label="t('validation.setting.serverUrl.localhost')"
/>
<Banner
v-for="(err, i) in fvGetPathErrors(['serverUrl'])"
:key="i"
color="error"
:label="err"
/>
<LabeledInput
v-model="serverUrl"
:label="t('setup.serverUrl.label')"
data-testid="setup-server-url"
:rules="fvGetAndReportPathRules('serverUrl')"
:required="true"
@input="onServerUrlChange"
/>
</div>
</template>

View File

@ -8,7 +8,6 @@ import { cronSchedule } from '@shell/utils/validators/cron-schedule';
import { podAffinity } from '@shell/utils/validators/pod-affinity';
import { roleTemplateRules } from '@shell/utils/validators/role-template';
import { clusterName } from '@shell/utils/validators/cluster-name';
import { isHttps } from '@shell/utils/validators/setting';
/**
* Custom validation functions beyond normal scalr types
@ -30,5 +29,4 @@ export default {
cronSchedule,
podAffinity,
roleTemplateRules,
isHttps,
};

View File

@ -43,21 +43,66 @@ describe('formRules', () => {
expect(formRuleResult).toStrictEqual(expectedResult);
});
it('"isHttps" : returns undefined when valid https url value is supplied', () => {
it('"https" : returns undefined when valid https url value is supplied', () => {
const testValue = 'https://url.com';
const formRuleResult = formRules.isHttps('server-url')(testValue);
const formRuleResult = formRules.https(testValue);
expect(formRuleResult).toBeUndefined();
});
it('"isHttps" : returns correct message when http url value is supplied', () => {
it('"https" : returns correct message when http url value is supplied', () => {
const testValue = 'http://url.com';
const formRuleResult = formRules.isHttps('server-url')(testValue);
const formRuleResult = formRules.https(testValue);
const expectedResult = JSON.stringify({ message: 'validation.setting.serverUrl.https' });
expect(formRuleResult).toStrictEqual(expectedResult);
});
describe('localhost', () => {
const message = JSON.stringify({ message: 'validation.setting.serverUrl.localhost' });
const testCases = [
['http://LOCALhosT:8005', message],
['http://localhost:8005', message],
['https://localhost:8005', message],
['localhost', message],
['http://127.0.0.1', message],
['https://127.0.0.1', message],
['127.0.0.1', message],
['https://test.com', undefined],
['https://test.com/localhost', undefined],
[undefined, undefined]
];
it.each(testCases)(
'should return undefined or correct message based on the provided url',
(url, expected) => {
const formRuleResult = formRules.localhost(url);
expect(formRuleResult).toStrictEqual(expected);
}
);
});
describe('trailingForwardSlash', () => {
const message = JSON.stringify({ message: 'validation.setting.serverUrl.trailingForwardSlash' });
const testCases = [
['https://test.com', undefined],
['https://test.com/', message],
['https://', undefined],
['/', undefined],
[undefined, undefined]
];
it.each(testCases)(
'should return undefined or correct message based on the provided url',
(url, expected) => {
const formRuleResult = formRules.trailingForwardSlash(url);
expect(formRuleResult).toStrictEqual(expected);
}
);
});
it('"interval" : returns undefined when valid hour interval value is supplied', () => {
const testValue = '5h';
const formRuleResult = formRules.interval(testValue);

View File

@ -2,9 +2,11 @@ import { RBAC } from '@shell/config/types';
import { HCI } from '@shell/config/labels-annotations';
import isEmpty from 'lodash/isEmpty';
import has from 'lodash/has';
import isUrl from 'is-url';
// import uniq from 'lodash/uniq';
import cronstrue from 'cronstrue';
import { Translation } from '@shell/types/t';
import { isHttps, isLocalhost, hasTrailingForwardSlash } from '@shell/utils/validators/setting';
// import uniq from 'lodash/uniq';
export type Validator<T = undefined | string> = (val: any, arg?: any) => T;
@ -34,10 +36,6 @@ export class Port {
}
}
const httpsKeys = [
'server-url'
];
const runValidators = (val: any, validators: Validator[]) => {
for (const validator of validators) {
const message = validator(val);
@ -139,11 +137,13 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
}
};
const isHttps: ValidatorFactory = (key: string) => {
const isHttps: Validator = (val: string) => httpsKeys.includes(key) && !val.toLowerCase().startsWith('https://') ? t('validation.setting.serverUrl.https') : undefined;
const https: Validator = (val: string) => val && !isHttps(val) ? t('validation.setting.serverUrl.https') : undefined;
return isHttps;
};
const localhost: Validator = (val: string) => isLocalhost(val) ? t('validation.setting.serverUrl.localhost') : undefined;
const trailingForwardSlash: Validator = (val: string) => hasTrailingForwardSlash(val) ? t('validation.setting.serverUrl.trailingForwardSlash') : undefined;
const url: Validator = (val: string) => val && !isUrl(val) ? t('validation.setting.serverUrl.url') : undefined;
const interval: Validator = (val: string) => !/^\d+[hms]$/.test(val) ? t('validation.monitoring.route.interval', { key }) : undefined;
@ -475,7 +475,10 @@ export default function(t: Translation, { key = 'Value' }: ValidationOptions): {
hostname,
imageUrl,
interval,
isHttps,
https,
localhost,
trailingForwardSlash,
url,
matching,
maxLength,
maxValue,

View File

@ -1,13 +1,9 @@
const httpsKeys = [
'server-url'
];
import isUrl from 'is-url';
export function isHttps(value, getters, errors, validatorArgs, displayKey) {
const key = validatorArgs[0];
export const isServerUrl = (value) => value === 'server-url';
if (httpsKeys.includes(key) && !value.toLowerCase().startsWith('https://')) {
errors.push(getters['i18n/t']('validation.setting.serverUrl.https'));
}
export const isHttps = (value) => value.toLowerCase().startsWith('https://');
return errors;
}
export const isLocalhost = (value) => (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)/i).test(value);
export const hasTrailingForwardSlash = (value) => isUrl(value) && value?.toLowerCase().endsWith('/');

View File

@ -2877,6 +2877,11 @@
dependencies:
"@types/node" "*"
"@types/is-url@1.2.30":
version "1.2.30"
resolved "https://registry.yarnpkg.com/@types/is-url/-/is-url-1.2.30.tgz#85567e8bee4fee69202bc3448f9fb34b0d56c50a"
integrity sha512-AnlNFwjzC8XLda5VjRl4ItSd8qp8pSNowvsut0WwQyBWHpOxjxRJm8iO6uETWqEyLdYdb9/1j+Qd9gQ4l5I4fw==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"