From 0a15a0d0b729b9d17d9289cebad858425a663f35 Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Thu, 16 Jul 2026 00:14:15 +0530 Subject: [PATCH 1/7] Add IP and Referrer restriction validators to constraintValidator - Add isValidPermittedIPList and isValidPermittedRefererList for validating DevPortal API key Security Restriction inputs, with rules derived from what the gateway accepts (ApiKeyAuthenticatorUtils#validateAPIKeyRestrictions): - IP: comma-separated IPv4/IPv6 addresses or CIDR ranges, using the same address patterns as the Admin Portal blacklist form - Referrer: comma-separated URL/host/wildcard patterns; the gateway matches entries literally with '*' wildcards, so each entry must be non-empty, whitespace-free, and contain one of '.', ':', '/', '*' --- .../Tests/Unit/constraintValidator.test.js | 68 ++++++++++++++++- .../Shared/AppsAndKeys/constraintValidator.js | 73 +++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/portals/devportal/src/main/webapp/source/Tests/Unit/constraintValidator.test.js b/portals/devportal/src/main/webapp/source/Tests/Unit/constraintValidator.test.js index e11aff4f6b8..0611d8c2b7b 100644 --- a/portals/devportal/src/main/webapp/source/Tests/Unit/constraintValidator.test.js +++ b/portals/devportal/src/main/webapp/source/Tests/Unit/constraintValidator.test.js @@ -16,7 +16,7 @@ * under the License. */ -import validateConstraint, { VALIDATOR_TYPES, getConstraintHint } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; +import validateConstraint, { VALIDATOR_TYPES, getConstraintHint, isValidPermittedIPList, isValidPermittedRefererList } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; const mockIntl = { formatMessage: (msg, values) => { @@ -220,4 +220,70 @@ describe('constraintValidator', () => { expect(getConstraintHint(constraint, mockIntl, mockMessages)).toBe(''); }); }); +}); + +describe('API key restriction validators', () => { + describe('isValidPermittedIPList', () => { + it.each([ + ['192.168.1.100'], + ['0.0.0.0'], + ['255.255.255.255'], + ['10.0.0.0/24'], + ['192.168.1.0/32'], + ['2001:db8::1'], + ['::1'], + ['fe80::1%eth0'], + ['2001:db8::/64'], + ['::ffff:192.168.1.1'], + ['192.168.1.100, 10.0.0.0/24'], + ['192.168.1.100,2001:db8::1, 172.16.0.0/12'], + [' 192.168.1.100 '], + ])('should accept %s', (value) => { + expect(isValidPermittedIPList(value)).toBe(true); + }); + + it.each([ + [''], + [' '], + ['randomstring'], + ['999.1.1.1'], + ['192.168.1'], + ['192.168.1.100.5'], + ['192.168.1.100/33'], + ['2001:db8::/129'], + ['192.168.1.100/'], + ['192.168.1.100, notanip'], + ['192.168.1.100,,10.0.0.1'], + ['192.168.1.100 10.0.0.1'], + ['2001:zz8::1'], + ])('should reject %s', (value) => { + expect(isValidPermittedIPList(value)).toBe(false); + }); + }); + + describe('isValidPermittedRefererList', () => { + it.each([ + ['https://example.com'], + ['https://example.com/*'], + ['www.example.com/path'], + ['*.example.com'], + ['example.com'], + ['localhost:3000/*'], + ['https://a.com, https://b.com/*'], + [' https://example.com '], + ])('should accept %s', (value) => { + expect(isValidPermittedRefererList(value)).toBe(true); + }); + + it.each([ + [''], + [' '], + ['randomstring'], + ['https://example.com, randomstring'], + ['https://exa mple.com'], + ['https://a.com,,https://b.com'], + ])('should reject %s', (value) => { + expect(isValidPermittedRefererList(value)).toBe(false); + }); + }); }); \ No newline at end of file diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js index 54da95960c6..3bea9f4b108 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js @@ -151,3 +151,76 @@ const validateConstraint = (inputValue, constraint, intl, messages) => { }; export default validateConstraint; + +/* + * Validators for the API Key Security Restriction inputs (permittedIP / permittedReferer). + * + * The gateway treats both values as comma separated lists + * (org.wso2.carbon.apimgt.gateway.utils.ApiKeyAuthenticatorUtils#validateAPIKeyRestrictions): + * - Each permittedIP entry is matched via APIUtil.isIpInNetwork, which accepts plain + * IPv4/IPv6 addresses as well as CIDR notation (e.g. 10.0.0.0/24). + * - Each permittedReferer entry is matched literally against the Referer header, + * with '*' acting as a wildcard (e.g. https://example.com/*). + * + * The IPv4/IPv6 patterns below are the same patterns used by the Admin Portal + * (Throttling/Blacklist/AddEdit.jsx) so that both portals accept identical formats. + */ + +const IPV4_PATTERN = '(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}' + + '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; +const IPV6_PATTERN = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' + + '{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}' + + '(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}' + + '(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)' + + '|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,' + + '1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:(' + + '(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))'; + +const IPV4_REGEX = new RegExp(`^${IPV4_PATTERN}$`); +const IPV6_REGEX = new RegExp(`^${IPV6_PATTERN}$`); +const IPV4_CIDR_REGEX = new RegExp(`^${IPV4_PATTERN}/([0-9]|[12][0-9]|3[0-2])$`); +const IPV6_CIDR_REGEX = new RegExp(`^${IPV6_PATTERN}/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8])$`); + +/** + * Validates the value entered for an IP based API key restriction. + * Accepts a single entry or a comma separated list, where each entry is a valid + * IPv4 address, IPv6 address, or a CIDR range of either family. + * + * @param {string} value - The raw user input. + * @returns {boolean} true if every entry is a valid IP address or CIDR range. + */ +export const isValidPermittedIPList = (value) => { + if (!value || !value.trim()) { + return false; + } + return value.split(',').every((entry) => { + const ip = entry.trim(); + return ip !== '' + && (IPV4_REGEX.test(ip) + || IPV6_REGEX.test(ip) + || IPV4_CIDR_REGEX.test(ip) + || IPV6_CIDR_REGEX.test(ip)); + }); +}; + +/** + * Validates the value entered for a referrer based API key restriction. + * Accepts a single entry or a comma separated list. The gateway matches each entry + * literally (with '*' wildcards) against the Referer header, so entries are kept + * permissive: each one must be non empty, contain no whitespace, and look like a + * URL, host or wildcard pattern (contain at least one of '.', ':', '/' or '*'). + * + * @param {string} value - The raw user input. + * @returns {boolean} true if every entry is a plausible referrer pattern. + */ +export const isValidPermittedRefererList = (value) => { + if (!value || !value.trim()) { + return false; + } + return value.split(',').every((entry) => { + const referer = entry.trim(); + return referer !== '' + && !/\s/.test(referer) + && /[.:/*]/.test(referer); + }); +}; \ No newline at end of file From 19705eb84609af59fac8d669946b22fdd524b36e Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Thu, 16 Jul 2026 00:15:25 +0530 Subject: [PATCH 2/7] Validate API key restriction inputs in DevPortal key generation - Reject invalid Security Restriction values before generating an API key, in both the per-API key dialog (ApiKeyGenerate/ApiKeyListing) and the legacy application key dialog (LegacyApiKeys). Previously any string was accepted and a key was generated with a restriction that could never match. - Show an inline field error alongside the existing alert toast, clear it when the value or restriction type changes, and update the field placeholders to document the accepted formats. --- .../main/webapp/site/public/locales/en.json | 12 ++++-- .../Apis/Details/APIKeys/ApiKeyGenerate.jsx | 28 ++++++++++++++ .../Apis/Details/APIKeys/ApiKeyListing.jsx | 14 +++++-- .../Shared/AppsAndKeys/LegacyApiKeys.jsx | 38 +++++++++++++++++-- 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/portals/devportal/src/main/webapp/site/public/locales/en.json b/portals/devportal/src/main/webapp/site/public/locales/en.json index 7a88d84e4f7..006e27c0623 100644 --- a/portals/devportal/src/main/webapp/site/public/locales/en.json +++ b/portals/devportal/src/main/webapp/site/public/locales/en.json @@ -48,6 +48,8 @@ "Apis.Details.APIKeys.ApiKeyGenerate.alert.enterName": "Please enter a name for the API key.", "Apis.Details.APIKeys.ApiKeyGenerate.alert.enterRestrictionValue": "Please enter a {restrictionLabel} value.", "Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidCustomDays": "Please enter a valid positive number of days for custom validity period.", + "Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction": "Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range, separating multiple values with commas.", + "Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction": "Invalid referrer. Enter a URL or a pattern with * wildcards (e.g. https://example.com/*), separating multiple values with commas.", "Apis.Details.APIKeys.ApiKeyGenerate.alert.regenerateFailed": "Failed to regenerate API Key. Please try again.", "Apis.Details.APIKeys.ApiKeyGenerate.button.close": "Close", "Apis.Details.APIKeys.ApiKeyGenerate.button.regenerate": "Regenerate", @@ -98,12 +100,12 @@ "Apis.Details.APIKeys.ApiKeyListing.input.days.label": "Days", "Apis.Details.APIKeys.ApiKeyListing.input.days.placeholder": "Enter days", "Apis.Details.APIKeys.ApiKeyListing.input.ipAddress.label": "IP Address", - "Apis.Details.APIKeys.ApiKeyListing.input.ipAddress.placeholder": "e.g. 192.168.1.100", + "Apis.Details.APIKeys.ApiKeyListing.input.ipAddress.placeholder": "e.g. 192.168.1.100, 10.0.0.0/24", "Apis.Details.APIKeys.ApiKeyListing.input.keyType.label": "Key Type", "Apis.Details.APIKeys.ApiKeyListing.input.name.label": "Name", "Apis.Details.APIKeys.ApiKeyListing.input.name.placeholder": "Enter a name for this API key", "Apis.Details.APIKeys.ApiKeyListing.input.referrerUrl.label": "Referrer URL", - "Apis.Details.APIKeys.ApiKeyListing.input.referrerUrl.placeholder": "e.g. https://example.com", + "Apis.Details.APIKeys.ApiKeyListing.input.referrerUrl.placeholder": "e.g. https://example.com/*", "Apis.Details.APIKeys.ApiKeyListing.input.securityRestriction.label": "Security Restriction", "Apis.Details.APIKeys.ApiKeyListing.input.validityPeriod.label": "Validity Period", "Apis.Details.APIKeys.ApiKeyListing.keyType.production": "Production", @@ -855,6 +857,8 @@ "Shared.AppsAndKeys.LegacyApiKeys.alert.enterRestrictionValue": "Please enter a {restrictionLabel} value.", "Shared.AppsAndKeys.LegacyApiKeys.alert.generateFailed": "Failed to generate API key. Please try again.", "Shared.AppsAndKeys.LegacyApiKeys.alert.invalidCustomDays": "Please enter a valid positive number of days for custom validity period.", + "Shared.AppsAndKeys.LegacyApiKeys.alert.invalidIPRestriction": "Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range, separating multiple values with commas.", + "Shared.AppsAndKeys.LegacyApiKeys.alert.invalidRefererRestriction": "Invalid referrer. Enter a URL or a pattern with * wildcards (e.g. https://example.com/*), separating multiple values with commas.", "Shared.AppsAndKeys.LegacyApiKeys.button.cancel": "Cancel", "Shared.AppsAndKeys.LegacyApiKeys.button.close": "Close", "Shared.AppsAndKeys.LegacyApiKeys.button.generate": "Generate Legacy API Key", @@ -874,11 +878,11 @@ "Shared.AppsAndKeys.LegacyApiKeys.field.days.label": "Days", "Shared.AppsAndKeys.LegacyApiKeys.field.days.placeholder": "Enter days", "Shared.AppsAndKeys.LegacyApiKeys.field.ipAddress.label": "IP Address", - "Shared.AppsAndKeys.LegacyApiKeys.field.ipAddress.placeholder": "e.g. 192.168.1.100", + "Shared.AppsAndKeys.LegacyApiKeys.field.ipAddress.placeholder": "e.g. 192.168.1.100, 10.0.0.0/24", "Shared.AppsAndKeys.LegacyApiKeys.field.name.label": "Name", "Shared.AppsAndKeys.LegacyApiKeys.field.name.placeholder": "Enter a name for this API key", "Shared.AppsAndKeys.LegacyApiKeys.field.referrerUrl.label": "Referrer URL", - "Shared.AppsAndKeys.LegacyApiKeys.field.referrerUrl.placeholder": "e.g. https://example.com", + "Shared.AppsAndKeys.LegacyApiKeys.field.referrerUrl.placeholder": "e.g. https://example.com/*", "Shared.AppsAndKeys.LegacyApiKeys.field.securityRestriction.label": "Security Restriction", "Shared.AppsAndKeys.LegacyApiKeys.field.validityPeriod.label": "Validity Period", "Shared.AppsAndKeys.LegacyApiKeys.generatedKey.alert.message": "Please copy this generated API Key value as it will be displayed only for the current browser session. (The API Key will not be visible in the UI after the page is refreshed.)", diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx index f7f4a523e14..0936fbc8c33 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx @@ -37,6 +37,10 @@ import { import { ContentCopy, Refresh } from '@mui/icons-material'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; +import { + isValidPermittedIPList, + isValidPermittedRefererList, +} from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; /** * Custom hook for managing API key generation and regeneration operations @@ -54,6 +58,7 @@ export default function ApiKeyGenerate(apiUUID, refreshApiKeys) { const [customValidityDays, setCustomValidityDays] = React.useState(''); const [restrictionType, setRestrictionType] = React.useState('none'); const [restrictionValue, setRestrictionValue] = React.useState(''); + const [restrictionError, setRestrictionError] = React.useState(''); // Generation modal state const [generationModalOpen, setGenerationModalOpen] = React.useState(false); @@ -140,6 +145,7 @@ export default function ApiKeyGenerate(apiUUID, refreshApiKeys) { setCustomValidityDays(''); setRestrictionType('none'); setRestrictionValue(''); + setRestrictionError(''); setShowToken(false); setApikey(null); // Refresh the API keys list after closing @@ -166,6 +172,26 @@ export default function ApiKeyGenerate(apiUUID, refreshApiKeys) { )); return; } + if (restrictionType === 'ip' && !isValidPermittedIPList(restrictionValue)) { + const message = intl.formatMessage({ + id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction', + defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' + + ' separating multiple values with commas.', + }); + setRestrictionError(message); + Alert.error(message); + return; + } + if (restrictionType === 'referrer' && !isValidPermittedRefererList(restrictionValue)) { + const message = intl.formatMessage({ + id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction', + defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' + + ' (e.g. https://example.com/*), separating multiple values with commas.', + }); + setRestrictionError(message); + Alert.error(message); + return; + } if (validityPeriod === 'custom' && !customValidityDays) { const customDays = Number(customValidityDays); if (!Number.isInteger(customDays) || customDays <= 0) { @@ -436,6 +462,8 @@ export default function ApiKeyGenerate(apiUUID, refreshApiKeys) { setRestrictionType, restrictionValue, setRestrictionValue, + restrictionError, + setRestrictionError, generationModalOpen, isGenerating, apikey, diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyListing.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyListing.jsx index a40e0193043..5eb62d8639b 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyListing.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyListing.jsx @@ -215,6 +215,8 @@ export default function ApiKeyListing() { setRestrictionType, restrictionValue, setRestrictionValue, + restrictionError, + setRestrictionError, generationModalOpen, isGenerating, apikey, @@ -743,6 +745,7 @@ export default function ApiKeyListing() { onChange={(e) => { setRestrictionType(e.target.value); setRestrictionValue(''); + setRestrictionError(''); }} > {restrictionOptions.map((option) => ( @@ -768,16 +771,21 @@ export default function ApiKeyListing() { defaultMessage: 'Referrer URL', })} value={restrictionValue} - onChange={(e) => setRestrictionValue(e.target.value)} + onChange={(e) => { + setRestrictionValue(e.target.value); + setRestrictionError(''); + }} placeholder={restrictionType === 'ip' ? intl.formatMessage({ id: 'Apis.Details.APIKeys.ApiKeyListing.input.ipAddress.placeholder', - defaultMessage: 'e.g. 192.168.1.100', + defaultMessage: 'e.g. 192.168.1.100, 10.0.0.0/24', }) : intl.formatMessage({ id: 'Apis.Details.APIKeys.ApiKeyListing.input.referrerUrl.placeholder', - defaultMessage: 'e.g. https://example.com', + defaultMessage: 'e.g. https://example.com/*', })} + error={Boolean(restrictionError)} + helperText={restrictionError} sx={{ minWidth: 200 }} /> diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx index 83544b40d33..e666cac1472 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx @@ -50,6 +50,10 @@ import { import MUIDataTable from 'mui-datatables'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; +import { + isValidPermittedIPList, + isValidPermittedRefererList, +} from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; /** * LegacyApiKeys component for managing legacy API keys @@ -89,6 +93,7 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { const [customValidityDays, setCustomValidityDays] = React.useState(''); const [restrictionType, setRestrictionType] = React.useState('none'); const [restrictionValue, setRestrictionValue] = React.useState(''); + const [restrictionError, setRestrictionError] = React.useState(''); /** * Gets all the legacy API keys for a particular application @@ -164,6 +169,7 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { setCustomValidityDays(''); setRestrictionType('none'); setRestrictionValue(''); + setRestrictionError(''); }; const handleCloseGeneratedKeyModal = () => { @@ -192,6 +198,26 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { )); return; } + if (restrictionType === 'ip' && !isValidPermittedIPList(restrictionValue)) { + const message = intl.formatMessage({ + id: 'Shared.AppsAndKeys.LegacyApiKeys.alert.invalidIPRestriction', + defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' + + ' separating multiple values with commas.', + }); + setRestrictionError(message); + Alert.error(message); + return; + } + if (restrictionType === 'referrer' && !isValidPermittedRefererList(restrictionValue)) { + const message = intl.formatMessage({ + id: 'Shared.AppsAndKeys.LegacyApiKeys.alert.invalidRefererRestriction', + defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' + + ' (e.g. https://example.com/*), separating multiple values with commas.', + }); + setRestrictionError(message); + Alert.error(message); + return; + } if (validityPeriod === 'custom' && !customValidityDays) { const customDays = Number(customValidityDays); if (!Number.isInteger(customDays) || customDays <= 0) { @@ -619,6 +645,7 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { onChange={(e) => { setRestrictionType(e.target.value); setRestrictionValue(''); + setRestrictionError(''); }} > {restrictionOptions.map((option) => ( @@ -638,10 +665,15 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { ? intl.formatMessage({ id: 'Shared.AppsAndKeys.LegacyApiKeys.field.ipAddress.label', defaultMessage: 'IP Address' }) : intl.formatMessage({ id: 'Shared.AppsAndKeys.LegacyApiKeys.field.referrerUrl.label', defaultMessage: 'Referrer URL' })} value={restrictionValue} - onChange={(e) => setRestrictionValue(e.target.value)} + onChange={(e) => { + setRestrictionValue(e.target.value); + setRestrictionError(''); + }} placeholder={restrictionType === 'ip' - ? intl.formatMessage({ id: 'Shared.AppsAndKeys.LegacyApiKeys.field.ipAddress.placeholder', defaultMessage: 'e.g. 192.168.1.100' }) - : intl.formatMessage({ id: 'Shared.AppsAndKeys.LegacyApiKeys.field.referrerUrl.placeholder', defaultMessage: 'e.g. https://example.com' })} + ? intl.formatMessage({ id: 'Shared.AppsAndKeys.LegacyApiKeys.field.ipAddress.placeholder', defaultMessage: 'e.g. 192.168.1.100, 10.0.0.0/24' }) + : intl.formatMessage({ id: 'Shared.AppsAndKeys.LegacyApiKeys.field.referrerUrl.placeholder', defaultMessage: 'e.g. https://example.com/*' })} + error={Boolean(restrictionError)} + helperText={restrictionError} /> )} From 46d1211e71bd0601cebcff2bfa9fc39f0df155b7 Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Sat, 18 Jul 2026 19:46:06 +0530 Subject: [PATCH 3/7] fix: Extract shared restriction validation to address SonarCloud findings --- .../Apis/Details/APIKeys/ApiKeyGenerate.jsx | 43 +++++---- .../Shared/AppsAndKeys/LegacyApiKeys.jsx | 41 ++++----- .../Shared/AppsAndKeys/constraintValidator.js | 90 ++++++++++++------- 3 files changed, 95 insertions(+), 79 deletions(-) diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx index 0936fbc8c33..6e23ba6a202 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx @@ -17,7 +17,7 @@ */ import React from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { Alert as MuiAlert, Box, @@ -37,10 +37,21 @@ import { import { ContentCopy, Refresh } from '@mui/icons-material'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; -import { - isValidPermittedIPList, - isValidPermittedRefererList, -} from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; +import { validateRestrictionValue } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; + +// Message descriptors for validation errors +const restrictionMessages = defineMessages({ + invalidIPRestriction: { + id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction', + defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' + + ' separating multiple values with commas.', + }, + invalidRefererRestriction: { + id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction', + defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' + + ' (e.g. https://example.com/*), separating multiple values with commas.', + }, +}) /** * Custom hook for managing API key generation and regeneration operations @@ -172,24 +183,10 @@ export default function ApiKeyGenerate(apiUUID, refreshApiKeys) { )); return; } - if (restrictionType === 'ip' && !isValidPermittedIPList(restrictionValue)) { - const message = intl.formatMessage({ - id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction', - defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' - + ' separating multiple values with commas.', - }); - setRestrictionError(message); - Alert.error(message); - return; - } - if (restrictionType === 'referrer' && !isValidPermittedRefererList(restrictionValue)) { - const message = intl.formatMessage({ - id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction', - defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' - + ' (e.g. https://example.com/*), separating multiple values with commas.', - }); - setRestrictionError(message); - Alert.error(message); + const restrictionMessage = validateRestrictionValue(restrictionType, restrictionValue, intl, restrictionMessages); + if (restrictionMessage) { + setRestrictionError(restrictionMessage); + Alert.error(restrictionMessage); return; } if (validityPeriod === 'custom' && !customValidityDays) { diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx index e666cac1472..fb6ff22f505 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx @@ -50,10 +50,21 @@ import { import MUIDataTable from 'mui-datatables'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; -import { - isValidPermittedIPList, - isValidPermittedRefererList, -} from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; +import { validateRestrictionValue } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; + +// Message descriptors for validation errors +const restrictionMessages = defineMessages({ + invalidIPRestriction: { + id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction', + defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' + + ' separating multiple values with commas.', + }, + invalidRefererRestriction: { + id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction', + defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' + + ' (e.g. https://example.com/*), separating multiple values with commas.', + }, +}) /** * LegacyApiKeys component for managing legacy API keys @@ -198,24 +209,10 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { )); return; } - if (restrictionType === 'ip' && !isValidPermittedIPList(restrictionValue)) { - const message = intl.formatMessage({ - id: 'Shared.AppsAndKeys.LegacyApiKeys.alert.invalidIPRestriction', - defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' - + ' separating multiple values with commas.', - }); - setRestrictionError(message); - Alert.error(message); - return; - } - if (restrictionType === 'referrer' && !isValidPermittedRefererList(restrictionValue)) { - const message = intl.formatMessage({ - id: 'Shared.AppsAndKeys.LegacyApiKeys.alert.invalidRefererRestriction', - defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' - + ' (e.g. https://example.com/*), separating multiple values with commas.', - }); - setRestrictionError(message); - Alert.error(message); + const restrictionMessage = validateRestrictionValue(restrictionType, restrictionValue, intl, restrictionMessages); + if (restrictionMessage) { + setRestrictionError(restrictionMessage); + Alert.error(restrictionMessage); return; } if (validityPeriod === 'custom' && !customValidityDays) { diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js index 3bea9f4b108..bba83b4d532 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js @@ -42,6 +42,35 @@ export const VALIDATOR_TYPES = { REGEX: 'REGEX', }; +/* + * Validators for the API Key Security Restriction inputs (permittedIP / permittedReferer). + * + * The gateway treats both values as comma separated lists + * (org.wso2.carbon.apimgt.gateway.utils.ApiKeyAuthenticatorUtils#validateAPIKeyRestrictions): + * - Each permittedIP entry is matched via APIUtil.isIpInNetwork, which accepts plain + * IPv4/IPv6 addresses as well as CIDR notation (e.g. 10.0.0.0/24). + * - Each permittedReferer entry is matched literally against the Referer header, + * with '*' acting as a wildcard (e.g. https://example.com/*). + * + * The IPv4/IPv6 patterns below are the same patterns used by the Admin Portal + * (Throttling/Blacklist/AddEdit.jsx) so that both portals accept identical formats. + */ + +const IPV4_PATTERN = String.raw`(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}` + + String.raw`([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])`; +const IPV6_PATTERN = String.raw`(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)` + + String.raw`{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}` + + String.raw`(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}` + + String.raw`(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)` + + String.raw`|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,` + + String.raw`1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:(` + + String.raw`(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`; + +const IPV4_REGEX = new RegExp(`^${IPV4_PATTERN}$`); +const IPV6_REGEX = new RegExp(`^${IPV6_PATTERN}$`); +const IPV4_CIDR_REGEX = new RegExp(`^${IPV4_PATTERN}/([0-9]|[12][0-9]|3[0-2])$`); +const IPV6_CIDR_REGEX = new RegExp(`^${IPV6_PATTERN}/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8])$`); + /** * Returns a localized hint string for a constraint. * @param {object|null|undefined} constraint - The constraint object from config. @@ -150,37 +179,6 @@ const validateConstraint = (inputValue, constraint, intl, messages) => { } }; -export default validateConstraint; - -/* - * Validators for the API Key Security Restriction inputs (permittedIP / permittedReferer). - * - * The gateway treats both values as comma separated lists - * (org.wso2.carbon.apimgt.gateway.utils.ApiKeyAuthenticatorUtils#validateAPIKeyRestrictions): - * - Each permittedIP entry is matched via APIUtil.isIpInNetwork, which accepts plain - * IPv4/IPv6 addresses as well as CIDR notation (e.g. 10.0.0.0/24). - * - Each permittedReferer entry is matched literally against the Referer header, - * with '*' acting as a wildcard (e.g. https://example.com/*). - * - * The IPv4/IPv6 patterns below are the same patterns used by the Admin Portal - * (Throttling/Blacklist/AddEdit.jsx) so that both portals accept identical formats. - */ - -const IPV4_PATTERN = '(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}' - + '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; -const IPV6_PATTERN = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' - + '{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}' - + '(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}' - + '(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)' - + '|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,' - + '1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:(' - + '(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))'; - -const IPV4_REGEX = new RegExp(`^${IPV4_PATTERN}$`); -const IPV6_REGEX = new RegExp(`^${IPV6_PATTERN}$`); -const IPV4_CIDR_REGEX = new RegExp(`^${IPV4_PATTERN}/([0-9]|[12][0-9]|3[0-2])$`); -const IPV6_CIDR_REGEX = new RegExp(`^${IPV6_PATTERN}/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8])$`); - /** * Validates the value entered for an IP based API key restriction. * Accepts a single entry or a comma separated list, where each entry is a valid @@ -190,7 +188,7 @@ const IPV6_CIDR_REGEX = new RegExp(`^${IPV6_PATTERN}/([0-9]|[1-9][0-9]|1[01][0-9 * @returns {boolean} true if every entry is a valid IP address or CIDR range. */ export const isValidPermittedIPList = (value) => { - if (!value || !value.trim()) { + if (!value?.trim()) { return false; } return value.split(',').every((entry) => { @@ -214,7 +212,7 @@ export const isValidPermittedIPList = (value) => { * @returns {boolean} true if every entry is a plausible referrer pattern. */ export const isValidPermittedRefererList = (value) => { - if (!value || !value.trim()) { + if (!value?.trim()) { return false; } return value.split(',').every((entry) => { @@ -223,4 +221,28 @@ export const isValidPermittedRefererList = (value) => { && !/\s/.test(referer) && /[.:/*]/.test(referer); }); -}; \ No newline at end of file +}; + +/** + * Validates the value entered for an API key Security Restriction and returns a + * localized error message, or an empty string when the value is acceptable. + * Message descriptors are supplied by the caller (same pattern as validateConstraint) + * so that their IDs remain extractable from the calling .jsx component. + * + * @param {string} restrictionType - The selected restriction type ('none', 'ip' or 'referrer'). + * @param {string} restrictionValue - The raw user input. + * @param {object} intl - The intl object for formatMessage. + * @param {object} messages - defineMessages result with invalidIPRestriction and invalidRefererRestriction. + * @returns {string} Localized error message, or '' if valid. + */ +export const validateRestrictionValue = (restrictionType, restrictionValue, intl, messages) => { + if (restrictionType === 'ip' && !isValidPermittedIPList(restrictionValue)) { + return intl.formatMessage(messages.invalidIPRestriction); + } + if (restrictionType === 'referrer' && !isValidPermittedRefererList(restrictionValue)) { + return intl.formatMessage(messages.invalidRefererRestriction); + } + return ''; +}; + +export default validateConstraint; \ No newline at end of file From fc8a7ad37eb9322a7bde7e9cadbe71be84e938dc Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Sat, 18 Jul 2026 23:21:36 +0530 Subject: [PATCH 4/7] fix: share restriction validation messages to resolve remaining duplication --- .../src/main/webapp/site/public/locales/en.json | 4 +--- .../Apis/Details/APIKeys/ApiKeyGenerate.jsx | 17 ++--------------- .../Shared/AppsAndKeys/LegacyApiKeys.jsx | 10 +++++----- .../Shared/AppsAndKeys/constraintValidator.js | 12 ++++++------ 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/portals/devportal/src/main/webapp/site/public/locales/en.json b/portals/devportal/src/main/webapp/site/public/locales/en.json index 006e27c0623..ac129b88e94 100644 --- a/portals/devportal/src/main/webapp/site/public/locales/en.json +++ b/portals/devportal/src/main/webapp/site/public/locales/en.json @@ -48,8 +48,6 @@ "Apis.Details.APIKeys.ApiKeyGenerate.alert.enterName": "Please enter a name for the API key.", "Apis.Details.APIKeys.ApiKeyGenerate.alert.enterRestrictionValue": "Please enter a {restrictionLabel} value.", "Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidCustomDays": "Please enter a valid positive number of days for custom validity period.", - "Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction": "Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range, separating multiple values with commas.", - "Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction": "Invalid referrer. Enter a URL or a pattern with * wildcards (e.g. https://example.com/*), separating multiple values with commas.", "Apis.Details.APIKeys.ApiKeyGenerate.alert.regenerateFailed": "Failed to regenerate API Key. Please try again.", "Apis.Details.APIKeys.ApiKeyGenerate.button.close": "Close", "Apis.Details.APIKeys.ApiKeyGenerate.button.regenerate": "Regenerate", @@ -1090,4 +1088,4 @@ "subscription.pending": "Your subscription request has been submitted and is now awaiting approval.", "subscription.tierPending": "Your subscription update request has been submitted and is now awaiting approval.", "username": "Username" -} +} \ No newline at end of file diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx index 6e23ba6a202..f9aa6e64623 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx @@ -17,7 +17,7 @@ */ import React from 'react'; -import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { Alert as MuiAlert, Box, @@ -38,20 +38,7 @@ import { ContentCopy, Refresh } from '@mui/icons-material'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; import { validateRestrictionValue } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; - -// Message descriptors for validation errors -const restrictionMessages = defineMessages({ - invalidIPRestriction: { - id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction', - defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' - + ' separating multiple values with commas.', - }, - invalidRefererRestriction: { - id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction', - defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' - + ' (e.g. https://example.com/*), separating multiple values with commas.', - }, -}) +import { restrictionMessages } from 'AppComponents/Shared/AppsAndKeys/LegacyApiKeys'; /** * Custom hook for managing API key generation and regeneration operations diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx index fb6ff22f505..c58cdc5edb2 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx @@ -52,19 +52,19 @@ import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; import { validateRestrictionValue } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; -// Message descriptors for validation errors -const restrictionMessages = defineMessages({ +// Message descriptors for the Security Restriction validation errors, shared with the per-API key generation dialog (ApiKeyGenerate). +export const restrictionMessages = defineMessages({ invalidIPRestriction: { - id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidIPRestriction', + id: 'Shared.AppsAndKeys.LegacyApiKeys.alert.invalidIPRestriction', defaultMessage: 'Invalid IP address. Enter a valid IPv4/IPv6 address or CIDR range,' + ' separating multiple values with commas.', }, invalidRefererRestriction: { - id: 'Apis.Details.APIKeys.ApiKeyGenerate.alert.invalidRefererRestriction', + id: 'Shared.AppsAndKeys.LegacyApiKeys.alert.invalidRefererRestriction', defaultMessage: 'Invalid referrer. Enter a URL or a pattern with * wildcards' + ' (e.g. https://example.com/*), separating multiple values with commas.', }, -}) +}); /** * LegacyApiKeys component for managing legacy API keys diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js index bba83b4d532..23a79807d33 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js @@ -57,12 +57,12 @@ export const VALIDATOR_TYPES = { */ const IPV4_PATTERN = String.raw`(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}` - + String.raw`([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])`; -const IPV6_PATTERN = String.raw`(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)` - + String.raw`{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}` - + String.raw`(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}` - + String.raw`(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)` - + String.raw`|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,` + + '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; +const IPV6_PATTERN = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' + + '{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}' + + '(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}' + + '(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)' + + '|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,' + String.raw`1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:(` + String.raw`(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`; From f96365eede5348fc671e9b5e88e400301a04ee64 Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Sun, 19 Jul 2026 00:25:05 +0530 Subject: [PATCH 5/7] fix: centralize API key restriction validation handling --- .../Apis/Details/APIKeys/ApiKeyGenerate.jsx | 10 ++-------- .../Shared/AppsAndKeys/LegacyApiKeys.jsx | 16 ++++++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx index f9aa6e64623..44a6c0fe7cf 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Details/APIKeys/ApiKeyGenerate.jsx @@ -37,8 +37,7 @@ import { import { ContentCopy, Refresh } from '@mui/icons-material'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; -import { validateRestrictionValue } from 'AppComponents/Shared/AppsAndKeys/constraintValidator'; -import { restrictionMessages } from 'AppComponents/Shared/AppsAndKeys/LegacyApiKeys'; +import { validateRestrictionOrAlert } from 'AppComponents/Shared/AppsAndKeys/LegacyApiKeys'; /** * Custom hook for managing API key generation and regeneration operations @@ -170,12 +169,7 @@ export default function ApiKeyGenerate(apiUUID, refreshApiKeys) { )); return; } - const restrictionMessage = validateRestrictionValue(restrictionType, restrictionValue, intl, restrictionMessages); - if (restrictionMessage) { - setRestrictionError(restrictionMessage); - Alert.error(restrictionMessage); - return; - } + if (!validateRestrictionOrAlert(restrictionType, restrictionValue, intl, setRestrictionError)) return; if (validityPeriod === 'custom' && !customValidityDays) { const customDays = Number(customValidityDays); if (!Number.isInteger(customDays) || customDays <= 0) { diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx index c58cdc5edb2..1008f8549af 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx @@ -66,6 +66,15 @@ export const restrictionMessages = defineMessages({ }, }); +export const validateRestrictionOrAlert = (restrictionType, restrictionValue, intl, setRestrictionError) => { + const message = validateRestrictionValue(restrictionType, restrictionValue, intl, restrictionMessages); + if (message) { + setRestrictionError(message); + Alert.error(message); + return false; + } + return true; +}; /** * LegacyApiKeys component for managing legacy API keys * @param {object} props - Component props @@ -209,12 +218,7 @@ export default function LegacyApiKeys({ keyType, selectedApp }) { )); return; } - const restrictionMessage = validateRestrictionValue(restrictionType, restrictionValue, intl, restrictionMessages); - if (restrictionMessage) { - setRestrictionError(restrictionMessage); - Alert.error(restrictionMessage); - return; - } + if (!validateRestrictionOrAlert(restrictionType, restrictionValue, intl, setRestrictionError)) return; if (validityPeriod === 'custom' && !customValidityDays) { const customDays = Number(customValidityDays); if (!Number.isInteger(customDays) || customDays <= 0) { From 0a6c296bacfaf98e5782e93a110bb252f9b4d1bf Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Sun, 19 Jul 2026 10:56:25 +0530 Subject: [PATCH 6/7] docs: Tidy validator comments - Shorten the comment blocks in constraintValidator and add a doc comment to validateRestrictionOrAlert --- .../Shared/AppsAndKeys/constraintValidator.js | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js index 23a79807d33..5577dfef17d 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/constraintValidator.js @@ -43,19 +43,11 @@ export const VALIDATOR_TYPES = { }; /* - * Validators for the API Key Security Restriction inputs (permittedIP / permittedReferer). - * - * The gateway treats both values as comma separated lists - * (org.wso2.carbon.apimgt.gateway.utils.ApiKeyAuthenticatorUtils#validateAPIKeyRestrictions): - * - Each permittedIP entry is matched via APIUtil.isIpInNetwork, which accepts plain - * IPv4/IPv6 addresses as well as CIDR notation (e.g. 10.0.0.0/24). - * - Each permittedReferer entry is matched literally against the Referer header, - * with '*' acting as a wildcard (e.g. https://example.com/*). - * - * The IPv4/IPv6 patterns below are the same patterns used by the Admin Portal - * (Throttling/Blacklist/AddEdit.jsx) so that both portals accept identical formats. + * Validators for API key security restrictions. + * IP validation supports IPv4, IPv6, and CIDR ranges. + * Referer values are validated separately and support '*' wildcards. + * The IPv4/IPv6 patterns are reused from the Admin Portal blacklist form (Throttling/Blacklist/AddEdit.jsx) so both portals accept the same formats. */ - const IPV4_PATTERN = String.raw`(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}` + '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; const IPV6_PATTERN = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' @@ -180,9 +172,7 @@ const validateConstraint = (inputValue, constraint, intl, messages) => { }; /** - * Validates the value entered for an IP based API key restriction. - * Accepts a single entry or a comma separated list, where each entry is a valid - * IPv4 address, IPv6 address, or a CIDR range of either family. + * Checks an IP based restriction value: a single IPv4/IPv6 address or CIDR range, or a comma separated list of them. * * @param {string} value - The raw user input. * @returns {boolean} true if every entry is a valid IP address or CIDR range. @@ -202,11 +192,8 @@ export const isValidPermittedIPList = (value) => { }; /** - * Validates the value entered for a referrer based API key restriction. - * Accepts a single entry or a comma separated list. The gateway matches each entry - * literally (with '*' wildcards) against the Referer header, so entries are kept - * permissive: each one must be non empty, contain no whitespace, and look like a - * URL, host or wildcard pattern (contain at least one of '.', ':', '/' or '*'). + * Validates a permitted referrer value. + * Supports single or comma-separated entries with '*' wildcards. * * @param {string} value - The raw user input. * @returns {boolean} true if every entry is a plausible referrer pattern. @@ -223,11 +210,8 @@ export const isValidPermittedRefererList = (value) => { }); }; -/** - * Validates the value entered for an API key Security Restriction and returns a - * localized error message, or an empty string when the value is acceptable. - * Message descriptors are supplied by the caller (same pattern as validateConstraint) - * so that their IDs remain extractable from the calling .jsx component. +/** + * Validates an API key security restriction value and returns the corresponding localized validation message. * * @param {string} restrictionType - The selected restriction type ('none', 'ip' or 'referrer'). * @param {string} restrictionValue - The raw user input. From e0a156132f2c57057025b53ba648de8e5806096c Mon Sep 17 00:00:00 2001 From: Gayuth-W Date: Sun, 19 Jul 2026 11:44:17 +0530 Subject: [PATCH 7/7] fix: clear restriction error state on successful validation --- .../src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx index 1008f8549af..58e132cce8d 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Shared/AppsAndKeys/LegacyApiKeys.jsx @@ -68,8 +68,8 @@ export const restrictionMessages = defineMessages({ export const validateRestrictionOrAlert = (restrictionType, restrictionValue, intl, setRestrictionError) => { const message = validateRestrictionValue(restrictionType, restrictionValue, intl, restrictionMessages); + setRestrictionError(message); if (message) { - setRestrictionError(message); Alert.error(message); return false; }