From 5cab769134e51225ff084d6752be740f47463f23 Mon Sep 17 00:00:00 2001 From: Stephane de Labrusse Date: Tue, 28 Jan 2025 15:55:59 +0100 Subject: [PATCH] feat(host set): add validation for IP range compatibility with port forwards (#496) NethServer/nethsecurity#1032 --- .../CreateOrEditPortForwardDrawer.vue | 29 ++++-- .../CreateOrEditHostSetDrawer.vue | 97 ++++++++++++++++++- src/composables/useObjects.ts | 1 + src/i18n/en/translation.json | 6 +- 4 files changed, 118 insertions(+), 15 deletions(-) diff --git a/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue b/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue index 67fa130c9..6760737cf 100644 --- a/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue +++ b/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue @@ -171,14 +171,17 @@ const restrictObjectsComboboxOptions = computed(() => { label: t('standalone.port_forward.no_object') } - const restrictOptions = props.restrictObjectSuggestions.map((obj) => { - return { - id: obj.id, - label: obj.name, - description: t(`standalone.objects.subtype_${obj.subtype}`), - icon: getObjectIcon(obj.subtype) - } - }) + // filter out objects that contain other objects in their ipaddr + const restrictOptions = props.restrictObjectSuggestions + .filter((obj) => !obj.ipaddr?.some((ip: string) => ip.includes('objects/'))) + .map((obj) => { + return { + id: obj.id, + label: obj.name, + description: t(`standalone.objects.subtype_${obj.subtype}`), + icon: getObjectIcon(obj.subtype) + } + }) return [noObjectOption, ...restrictOptions] }) @@ -667,7 +670,15 @@ async function createOrEditPortForward() { :selected-label="t('ne_combobox.selected')" :user-input-label="t('ne_combobox.user_input_label')" ref="destinationObjectRef" - /> + > + + import { useI18n } from 'vue-i18n' +import { cloneDeep } from 'lodash-es' import { NeInlineNotification, type NeComboboxOption, @@ -20,6 +21,7 @@ import { } from '@nethesis/vue-components' import { ref, type PropType, watch, type Ref, computed } from 'vue' import { ubusCall, ValidationError } from '@/lib/standalone/ubus' +import type { AxiosResponse } from 'axios' import { MessageBag, validateAlphanumeric, @@ -43,8 +45,21 @@ const props = defineProps({ } }) +type MatchInfo = { + database: string + family: 'ipv4' | 'ipv6' + id: string + name: string + type: string +} + +type MatchInfoResponse = AxiosResponse<{ + info: Record +}> + const emit = defineEmits(['close', 'reloadData']) +const portForwardsUsingHostSet = ref('') const { t } = useI18n() const name = ref('') const nameRef = ref() @@ -76,7 +91,13 @@ const ipVersionOptions = ref([ ]) const recordOptionsButCurrent = computed(() => { - return props.recordOptions?.filter((option) => option.id !== props.currentHostSet?.id) + // Filter out objects from recordOptions based on the presence of an IP address with a hyphen in allObjects + const objectsWithHyphenIp = props.allObjects + .filter((obj) => obj.ipaddr.some((ip: string) => ip.includes('-'))) + .map((obj) => obj.id) + return props.recordOptions?.filter( + (option) => option.id !== props.currentHostSet?.id && !objectsWithHyphenIp.includes(option.id) + ) }) const allObjectsButCurrent = computed(() => { @@ -94,7 +115,7 @@ watch( // editing host or host set name.value = props.currentHostSet.name ipVersion.value = props.currentHostSet.family as IpVersion - records.value = props.currentHostSet.ipaddr + records.value = cloneDeep(props.currentHostSet.ipaddr) // deep clone to avoid modifying the original array } else { // creating host or host set, reset form to defaults name.value = '' @@ -105,6 +126,18 @@ watch( } ) +// compute portForwardsUsingHostSet the name of the portforward rule using this object +watch( + () => props.currentHostSet?.matches, + async (matches) => { + if (matches) { + portForwardsUsingHostSet.value = await getMatchedItemsName(matches) + } else { + portForwardsUsingHostSet.value = '' + } + } +) + function closeDrawer() { emit('close') } @@ -133,6 +166,50 @@ function runFieldValidators( return validators.every((validator) => validator.valid) } +async function getMatchedItemsName(matches: string[]): Promise { + try { + const res: MatchInfoResponse = await ubusCall('ns.objects', 'get-info', { ids: matches }) + const names: string[] = [] + for (const match of Object.values(res.data.info)) { + if (match.type == 'redirect') { + names.push(match.name) + } + } + return names.join(', ') + } catch (error: any) { + console.error('Error fetching getMatchedItemsName:', error) + return '' + } +} + +function validateNoIpRangeWithPortForward(records: Array) { + for (const record of records) { + if (record.includes('-') && portForwardsUsingHostSet.value) { + return { + valid: false, + errMessage: 'standalone.objects.range_not_compatible_with_port_forward' + } + } + } + return { + valid: true + } +} + +function validateNoObjectsWithPortForward(records: Array) { + for (const record of records) { + if (record.includes('objects/') && portForwardsUsingHostSet.value) { + return { + valid: false, + errMessage: 'standalone.objects.objects_are_not_compatible_with_port_forward' + } + } + } + return { + valid: true + } +} + function validateHostSetNotExists(value: string) { if (allObjectsButCurrent.value?.find((obj) => obj.name === value && obj.subtype === 'host_set')) { return { @@ -158,7 +235,15 @@ function validate() { nameRef ], // records - [[validateRequired(records.value[0])], 'ipaddr', recordRef] + [ + [ + validateNoObjectsWithPortForward(records.value), + validateNoIpRangeWithPortForward(records.value), + validateRequired(records.value[0]) + ], + 'ipaddr', + recordRef + ] ] // reset firstErrorRef for focus management @@ -296,7 +381,11 @@ function deleteRecord(index: number) { v-if="errorBag.getFirstI18nKeyFor('ipaddr')" :class="'mt-2 text-sm text-rose-700 dark:text-rose-400'" > - {{ t(errorBag.getFirstI18nKeyFor('ipaddr')) }} + {{ + t(errorBag.getFirstI18nKeyFor('ipaddr'), { + name: portForwardsUsingHostSet + }) + }}