mirror of https://github.com/rancher/dashboard.git
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:
parent
5cf644c86e
commit
5919915b29
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}}'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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('/');
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue