Skip to content

Commit 551a544

Browse files
authored
{App Service} Fix: header-aware duplicate check for access-restriction add (#33272)
1 parent e0a26e4 commit 551a544

3 files changed

Lines changed: 426 additions & 12 deletions

File tree

src/azure-cli/azure/cli/command_modules/appservice/_validators.py

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,75 @@ def _validate_ip_address_existence(cmd, namespace):
319319
scm_site = namespace.scm_site
320320
configs = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_configuration', slot)
321321
access_rules = configs.scm_ip_security_restrictions if scm_site else configs.ip_security_restrictions
322-
ip_exists = [x.ip_address == namespace.ip_address for x in access_rules]
323-
if True in ip_exists:
324-
raise ArgumentUsageError('IP address: ' + namespace.ip_address + ' already exists. '
325-
'Cannot add duplicate IP address values.')
322+
new_headers = _normalize_http_headers(getattr(namespace, 'http_headers', None))
323+
new_ip_set = _normalize_ip_address_list(namespace.ip_address)
324+
for rule in access_rules or []:
325+
if _normalize_ip_address_list(rule.ip_address) != new_ip_set:
326+
continue
327+
if _normalize_http_headers(rule.headers) == new_headers:
328+
if not new_headers:
329+
raise ArgumentUsageError(
330+
"An access-restriction rule with IP address '{}' already exists. Cannot add a "
331+
"duplicate rule. Use a different IP address, or add a --http-header filter to "
332+
"create an additional rule.".format(namespace.ip_address))
333+
raise ArgumentUsageError(
334+
"An access-restriction rule with IP address '{}' and the same HTTP header filter "
335+
"already exists. Cannot add a duplicate rule. Use a different IP address or vary "
336+
"the --http-header values to create an additional rule.".format(namespace.ip_address))
337+
338+
339+
def _normalize_ip_address_list(ip_address):
340+
"""Return a frozenset of canonical CIDR strings parsed from a comma-separated input.
341+
342+
The CLI accepts up to 8 comma-separated CIDRs in a single rule's ``--ip-address``. ARM stores
343+
them in the same comma-separated form on the rule's ``ip_address`` attribute. Two rules
344+
represent the same source set regardless of the order in which the CIDRs appear, so duplicate
345+
detection should compare unordered sets. Returns ``frozenset()`` for missing/empty input.
346+
"""
347+
if not ip_address:
348+
return frozenset()
349+
return frozenset(part.strip() for part in ip_address.split(',') if part.strip())
350+
351+
352+
def _normalize_http_headers(headers):
353+
"""Normalize an http-header collection for duplicate-rule comparison.
354+
355+
Accepts either the CLI input form (list of "name=value" strings, as supplied via --http-header)
356+
or the SDK/ARM form (dict mapping header name to a list of values). Returns a dict whose keys
357+
are lowercased header names and whose values are frozensets of value strings, so that order of
358+
values within a single header (which ARM treats as a set) does not affect equality. None and
359+
an empty collection are both normalized to an empty dict so that a rule with no headers
360+
compares equal to itself regardless of representation.
361+
"""
362+
if not headers:
363+
return {}
364+
result = {}
365+
if isinstance(headers, dict):
366+
for header_name, values in headers.items():
367+
if header_name is None:
368+
continue
369+
normalized_name = header_name.strip().lower()
370+
if values is None:
371+
continue
372+
if isinstance(values, str):
373+
values = [values]
374+
value_set = frozenset(v for v in values if v)
375+
if value_set:
376+
result[normalized_name] = value_set
377+
return result
378+
# CLI input form: list of "name=value" strings.
379+
for header_str in headers:
380+
if header_str is None or '=' not in header_str:
381+
continue
382+
header_name, _, header_value = header_str.partition('=')
383+
normalized_name = header_name.strip().lower()
384+
normalized_value = header_value.strip()
385+
if not normalized_name or not normalized_value:
386+
continue
387+
existing = set(result.get(normalized_name, frozenset()))
388+
existing.add(normalized_value)
389+
result[normalized_name] = frozenset(existing)
390+
return result
326391

327392

328393
def validate_service_tag(cmd, namespace):
@@ -369,10 +434,21 @@ def _validate_service_tag_existence(cmd, namespace):
369434
input_tag_value = namespace.service_tag.replace(' ', '')
370435
configs = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get_configuration', slot)
371436
access_rules = configs.scm_ip_security_restrictions if scm_site else configs.ip_security_restrictions
372-
for rule in access_rules:
373-
if rule.ip_address and rule.ip_address.lower() == input_tag_value.lower():
374-
raise ArgumentUsageError('Service Tag: ' + namespace.service_tag + ' already exists. '
375-
'Cannot add duplicate Service Tag values.')
437+
new_headers = _normalize_http_headers(getattr(namespace, 'http_headers', None))
438+
for rule in access_rules or []:
439+
if not rule.ip_address or rule.ip_address.lower() != input_tag_value.lower():
440+
continue
441+
if _normalize_http_headers(rule.headers) == new_headers:
442+
if not new_headers:
443+
raise ArgumentUsageError(
444+
"A service-tag access-restriction rule with value '{}' already exists. Cannot "
445+
"add a duplicate rule. Use a different service tag, or add a --http-header "
446+
"filter to create an additional rule.".format(namespace.service_tag))
447+
raise ArgumentUsageError(
448+
"A service-tag access-restriction rule with value '{}' and the same HTTP header "
449+
"filter already exists. Cannot add a duplicate rule. Use a different service tag "
450+
"or vary the --http-header values to create an additional rule.".format(
451+
namespace.service_tag))
376452

377453

378454
def validate_public_cloud(cmd):

src/azure-cli/azure/cli/command_modules/appservice/access_restrictions.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from knack.log import get_logger
1414

1515
from ._appservice_utils import _generic_site_operation
16+
from ._validators import _normalize_http_headers
1617
from .custom import get_site_configs
1718

1819
logger = get_logger(__name__)
@@ -34,6 +35,10 @@ def show_webapp_access_restrictions(cmd, resource_group_name, name, slot=None):
3435
return access_rules
3536

3637

38+
# pylint: disable=too-many-locals
39+
# Adding the hoisted ``new_headers`` local plus the existing ones in this function
40+
# pushes the count just past pylint's default cap; the readability gain of computing
41+
# the normalized header dict once is worth the suppression.
3742
def add_webapp_access_restriction(
3843
cmd, resource_group_name, name, priority, rule_name=None,
3944
action='Allow', ip_address=None, subnet=None,
@@ -59,11 +64,24 @@ def add_webapp_access_restriction(
5964
subnet_id = _validate_subnet(cmd.cli_ctx, subnet, vnet_name, vnet_rg)
6065
if not ignore_missing_vnet_service_endpoint:
6166
_ensure_subnet_service_endpoint(cmd.cli_ctx, subnet_id)
62-
# check for duplicates
67+
# check for duplicates (header-aware: same subnet + identical http-header filter)
68+
new_headers = _normalize_http_headers(http_headers)
6369
for rule in list(access_rules):
64-
if rule.vnet_subnet_resource_id and rule.vnet_subnet_resource_id.lower() == subnet_id.lower():
65-
raise ArgumentUsageError('Service endpoint rule for: ' + subnet_id + ' already exists. '
66-
'Cannot add duplicate service endpoint rules.')
70+
if not rule.vnet_subnet_resource_id:
71+
continue
72+
if rule.vnet_subnet_resource_id.lower() != subnet_id.lower():
73+
continue
74+
if _normalize_http_headers(rule.headers) == new_headers:
75+
if not new_headers:
76+
raise ArgumentUsageError(
77+
"A service-endpoint access-restriction rule for subnet '{}' already "
78+
"exists. Cannot add a duplicate rule. Use a different subnet, or add a "
79+
"--http-header filter to create an additional rule.".format(subnet_id))
80+
raise ArgumentUsageError(
81+
"A service-endpoint access-restriction rule for subnet '{}' with the same "
82+
"HTTP header filter already exists. Cannot add a duplicate rule. Use a "
83+
"different subnet or vary the --http-header values to create an additional "
84+
"rule.".format(subnet_id))
6785
rule_instance = IpSecurityRestriction(
6886
name=rule_name, vnet_subnet_resource_id=subnet_id,
6987
priority=priority, action=action, tag='Default', description=description)

0 commit comments

Comments
 (0)