iam-policy-validator 1.14.7__py3-none-any.whl → 1.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/METADATA +16 -11
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/RECORD +41 -28
- iam_policy_validator-1.15.1.dist-info/entry_points.txt +4 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_validation.py +91 -27
- iam_validator/checks/not_action_not_resource.py +163 -0
- iam_validator/checks/resource_validation.py +132 -81
- iam_validator/checks/wildcard_resource.py +136 -6
- iam_validator/commands/__init__.py +3 -0
- iam_validator/commands/cache.py +66 -24
- iam_validator/commands/completion.py +94 -15
- iam_validator/commands/mcp.py +210 -0
- iam_validator/commands/query.py +489 -65
- iam_validator/core/aws_service/__init__.py +5 -1
- iam_validator/core/aws_service/cache.py +20 -0
- iam_validator/core/aws_service/fetcher.py +180 -11
- iam_validator/core/aws_service/storage.py +14 -6
- iam_validator/core/aws_service/validators.py +68 -51
- iam_validator/core/check_registry.py +100 -35
- iam_validator/core/config/aws_global_conditions.py +18 -9
- iam_validator/core/config/check_documentation.py +104 -51
- iam_validator/core/config/config_loader.py +39 -3
- iam_validator/core/config/defaults.py +6 -0
- iam_validator/core/constants.py +11 -4
- iam_validator/core/models.py +39 -14
- iam_validator/mcp/__init__.py +162 -0
- iam_validator/mcp/models.py +118 -0
- iam_validator/mcp/server.py +2928 -0
- iam_validator/mcp/session_config.py +319 -0
- iam_validator/mcp/templates/__init__.py +79 -0
- iam_validator/mcp/templates/builtin.py +856 -0
- iam_validator/mcp/tools/__init__.py +72 -0
- iam_validator/mcp/tools/generation.py +888 -0
- iam_validator/mcp/tools/org_config_tools.py +263 -0
- iam_validator/mcp/tools/query.py +395 -0
- iam_validator/mcp/tools/validation.py +376 -0
- iam_validator/sdk/__init__.py +2 -0
- iam_validator/sdk/policy_utils.py +31 -5
- iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,10 +12,14 @@ Example usage:
|
|
|
12
12
|
# Re-export main classes for public API
|
|
13
13
|
from iam_validator.core.aws_service.fetcher import AWSServiceFetcher
|
|
14
14
|
from iam_validator.core.aws_service.patterns import CompiledPatterns
|
|
15
|
-
from iam_validator.core.aws_service.validators import
|
|
15
|
+
from iam_validator.core.aws_service.validators import (
|
|
16
|
+
ConditionKeyValidationResult,
|
|
17
|
+
condition_key_in_list,
|
|
18
|
+
)
|
|
16
19
|
|
|
17
20
|
__all__ = [
|
|
18
21
|
"AWSServiceFetcher",
|
|
19
22
|
"ConditionKeyValidationResult",
|
|
20
23
|
"CompiledPatterns",
|
|
24
|
+
"condition_key_in_list",
|
|
21
25
|
]
|
|
@@ -70,6 +70,26 @@ class ServiceCacheManager:
|
|
|
70
70
|
|
|
71
71
|
return None
|
|
72
72
|
|
|
73
|
+
async def get_stale(self, url: str | None = None, base_url: str = "") -> Any | None:
|
|
74
|
+
"""Get from disk cache even if expired (stale data fallback).
|
|
75
|
+
|
|
76
|
+
Use this method to retrieve stale cache data when a fresh fetch fails.
|
|
77
|
+
The stale data can serve as a fallback to avoid complete failure.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
url: URL for disk cache lookup
|
|
81
|
+
base_url: Base URL for service reference API (used for disk cache path)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Cached data if found (regardless of TTL), None otherwise
|
|
85
|
+
"""
|
|
86
|
+
if url and self._storage:
|
|
87
|
+
cached = self._storage.read_from_cache(url, base_url, allow_stale=True)
|
|
88
|
+
if cached is not None:
|
|
89
|
+
logger.info(f"Using stale cache fallback for URL: {url}")
|
|
90
|
+
return cached
|
|
91
|
+
return None
|
|
92
|
+
|
|
73
93
|
async def set(
|
|
74
94
|
self, cache_key: str, value: Any, url: str | None = None, base_url: str = ""
|
|
75
95
|
) -> None:
|
|
@@ -245,11 +245,21 @@ class AWSServiceFetcher:
|
|
|
245
245
|
f"raw:{self.BASE_URL}", url=self.BASE_URL, base_url=self.BASE_URL
|
|
246
246
|
)
|
|
247
247
|
if data is None:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
try:
|
|
249
|
+
data = await self._client.fetch(self.BASE_URL)
|
|
250
|
+
# Cache the raw data (this refreshes the disk cache file)
|
|
251
|
+
await self._cache.set(
|
|
252
|
+
f"raw:{self.BASE_URL}", data, url=self.BASE_URL, base_url=self.BASE_URL
|
|
253
|
+
)
|
|
254
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
255
|
+
# API fetch failed - try stale cache as fallback
|
|
256
|
+
logger.warning(f"API fetch failed for services list: {e}")
|
|
257
|
+
stale_data = await self._cache.get_stale(url=self.BASE_URL, base_url=self.BASE_URL)
|
|
258
|
+
if stale_data is not None:
|
|
259
|
+
logger.info("Using stale cache data for services list due to API failure")
|
|
260
|
+
data = stale_data
|
|
261
|
+
else:
|
|
262
|
+
raise
|
|
253
263
|
|
|
254
264
|
if not isinstance(data, list):
|
|
255
265
|
raise ValueError("Expected list of services from root endpoint")
|
|
@@ -332,12 +342,26 @@ class AWSServiceFetcher:
|
|
|
332
342
|
f"raw:{service.url}", url=service.url, base_url=self.BASE_URL
|
|
333
343
|
)
|
|
334
344
|
if data is None:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
try:
|
|
346
|
+
# Fetch service detail from API
|
|
347
|
+
data = await self._client.fetch(service.url)
|
|
348
|
+
# Cache the raw data (this refreshes the disk cache file)
|
|
349
|
+
await self._cache.set(
|
|
350
|
+
f"raw:{service.url}", data, url=service.url, base_url=self.BASE_URL
|
|
351
|
+
)
|
|
352
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
353
|
+
# API fetch failed - try stale cache as fallback
|
|
354
|
+
logger.warning(f"API fetch failed for {service_name}: {e}")
|
|
355
|
+
stale_data = await self._cache.get_stale(
|
|
356
|
+
url=service.url, base_url=self.BASE_URL
|
|
357
|
+
)
|
|
358
|
+
if stale_data is not None:
|
|
359
|
+
logger.info(
|
|
360
|
+
f"Using stale cache data for {service_name} due to API failure"
|
|
361
|
+
)
|
|
362
|
+
data = stale_data
|
|
363
|
+
else:
|
|
364
|
+
raise
|
|
341
365
|
|
|
342
366
|
# Validate and parse
|
|
343
367
|
service_detail = ServiceDetail.model_validate(data)
|
|
@@ -421,6 +445,73 @@ class AWSServiceFetcher:
|
|
|
421
445
|
service_detail = await self.fetch_service_by_name(service_prefix)
|
|
422
446
|
return await self._validator.validate_action(action, service_detail, allow_wildcards)
|
|
423
447
|
|
|
448
|
+
async def validate_actions_batch(
|
|
449
|
+
self,
|
|
450
|
+
actions: list[str],
|
|
451
|
+
allow_wildcards: bool = True,
|
|
452
|
+
) -> dict[str, tuple[bool, str | None, bool]]:
|
|
453
|
+
"""Validate multiple IAM actions efficiently in batch.
|
|
454
|
+
|
|
455
|
+
Groups actions by service prefix and fetches each service definition once,
|
|
456
|
+
reducing network overhead when validating multiple actions.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
actions: List of full action strings (e.g., ["s3:GetObject", "iam:CreateUser"])
|
|
460
|
+
allow_wildcards: Whether to allow wildcard actions
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Dictionary mapping action -> (is_valid, error_message, is_wildcard)
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
467
|
+
... results = await fetcher.validate_actions_batch([
|
|
468
|
+
... "s3:GetObject",
|
|
469
|
+
... "s3:PutObject",
|
|
470
|
+
... "iam:CreateUser"
|
|
471
|
+
... ])
|
|
472
|
+
... for action, (is_valid, error, is_wildcard) in results.items():
|
|
473
|
+
... if not is_valid:
|
|
474
|
+
... print(f"Invalid: {action} - {error}")
|
|
475
|
+
"""
|
|
476
|
+
if not actions:
|
|
477
|
+
return {}
|
|
478
|
+
|
|
479
|
+
# Group actions by service prefix
|
|
480
|
+
service_actions: dict[str, list[str]] = {}
|
|
481
|
+
for action in actions:
|
|
482
|
+
service_prefix, _ = self._parser.parse_action(action)
|
|
483
|
+
if service_prefix not in service_actions:
|
|
484
|
+
service_actions[service_prefix] = []
|
|
485
|
+
service_actions[service_prefix].append(action)
|
|
486
|
+
|
|
487
|
+
# Fetch all service definitions in parallel
|
|
488
|
+
service_details: dict[str, ServiceDetail] = {}
|
|
489
|
+
fetch_tasks = [self.fetch_service_by_name(service) for service in service_actions.keys()]
|
|
490
|
+
fetched = await asyncio.gather(*fetch_tasks, return_exceptions=True)
|
|
491
|
+
|
|
492
|
+
for service, result in zip(service_actions.keys(), fetched):
|
|
493
|
+
if isinstance(result, BaseException):
|
|
494
|
+
# Store None to indicate fetch failure
|
|
495
|
+
service_details[service] = None # type: ignore
|
|
496
|
+
else:
|
|
497
|
+
service_details[service] = result
|
|
498
|
+
|
|
499
|
+
# Validate all actions using cached service details
|
|
500
|
+
results: dict[str, tuple[bool, str | None, bool]] = {}
|
|
501
|
+
for action in actions:
|
|
502
|
+
service_prefix, _ = self._parser.parse_action(action)
|
|
503
|
+
service_detail = service_details.get(service_prefix)
|
|
504
|
+
|
|
505
|
+
if service_detail is None:
|
|
506
|
+
# Service fetch failed
|
|
507
|
+
results[action] = (False, f"Failed to fetch service '{service_prefix}'", False)
|
|
508
|
+
else:
|
|
509
|
+
results[action] = await self._validator.validate_action(
|
|
510
|
+
action, service_detail, allow_wildcards
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return results
|
|
514
|
+
|
|
424
515
|
def validate_arn(self, arn: str) -> tuple[bool, str | None]:
|
|
425
516
|
"""Validate ARN format.
|
|
426
517
|
|
|
@@ -466,6 +557,84 @@ class AWSServiceFetcher:
|
|
|
466
557
|
action, condition_key, service_detail, resources
|
|
467
558
|
)
|
|
468
559
|
|
|
560
|
+
async def is_condition_key_supported(
|
|
561
|
+
self,
|
|
562
|
+
action: str,
|
|
563
|
+
condition_key: str,
|
|
564
|
+
) -> bool:
|
|
565
|
+
"""Check if a condition key is supported for a specific action.
|
|
566
|
+
|
|
567
|
+
This checks two locations for the condition key:
|
|
568
|
+
1. Action-level condition keys (ActionConditionKeys)
|
|
569
|
+
2. Resource-level condition keys (for each resource the action operates on)
|
|
570
|
+
|
|
571
|
+
Returns True if the condition key is found in either location.
|
|
572
|
+
|
|
573
|
+
This is useful for determining if a condition provides meaningful
|
|
574
|
+
restrictions for an action, particularly for resource-scoping conditions
|
|
575
|
+
like aws:ResourceTag/*.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
action: IAM action (e.g., "s3:GetObject", "ssm:StartSession")
|
|
579
|
+
condition_key: Condition key to check (e.g., "aws:ResourceTag/Env")
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
True if the condition key is supported for this action
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
586
|
+
... # SSM StartSession has aws:ResourceTag in ActionConditionKeys
|
|
587
|
+
... supported = await fetcher.is_condition_key_supported(
|
|
588
|
+
... "ssm:StartSession", "aws:ResourceTag/Component"
|
|
589
|
+
... )
|
|
590
|
+
... print(f"Tag support: {supported}") # True
|
|
591
|
+
"""
|
|
592
|
+
from iam_validator.core.aws_service.validators import ( # pylint: disable=import-outside-toplevel
|
|
593
|
+
condition_key_in_list,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
service_prefix, action_name = self._parser.parse_action(action)
|
|
598
|
+
except ValueError:
|
|
599
|
+
return False # Invalid action format
|
|
600
|
+
|
|
601
|
+
# Can't verify wildcard actions
|
|
602
|
+
if "*" in action_name or "?" in action_name:
|
|
603
|
+
return False
|
|
604
|
+
|
|
605
|
+
service_detail = await self.fetch_service_by_name(service_prefix)
|
|
606
|
+
if not service_detail:
|
|
607
|
+
return False
|
|
608
|
+
|
|
609
|
+
# Case-insensitive action lookup
|
|
610
|
+
action_detail = None
|
|
611
|
+
action_name_lower = action_name.lower()
|
|
612
|
+
for name, detail in service_detail.actions.items():
|
|
613
|
+
if name.lower() == action_name_lower:
|
|
614
|
+
action_detail = detail
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
if not action_detail:
|
|
618
|
+
return False
|
|
619
|
+
|
|
620
|
+
# Check 1: Action-level condition keys
|
|
621
|
+
if action_detail.action_condition_keys:
|
|
622
|
+
if condition_key_in_list(condition_key, action_detail.action_condition_keys):
|
|
623
|
+
return True
|
|
624
|
+
|
|
625
|
+
# Check 2: Resource-level condition keys
|
|
626
|
+
if action_detail.resources:
|
|
627
|
+
for res_ref in action_detail.resources:
|
|
628
|
+
resource_name = res_ref.get("Name")
|
|
629
|
+
if not resource_name:
|
|
630
|
+
continue
|
|
631
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
632
|
+
if resource_type and resource_type.condition_keys:
|
|
633
|
+
if condition_key_in_list(condition_key, resource_type.condition_keys):
|
|
634
|
+
return True
|
|
635
|
+
|
|
636
|
+
return False
|
|
637
|
+
|
|
469
638
|
# --- Parsing Methods (delegate to parser) ---
|
|
470
639
|
|
|
471
640
|
def parse_action(self, action: str) -> tuple[str, str]:
|
|
@@ -150,15 +150,16 @@ class ServiceFileStorage:
|
|
|
150
150
|
|
|
151
151
|
return self._cache_dir / filename
|
|
152
152
|
|
|
153
|
-
def read_from_cache(self, url: str, base_url: str) -> Any | None:
|
|
153
|
+
def read_from_cache(self, url: str, base_url: str, allow_stale: bool = False) -> Any | None:
|
|
154
154
|
"""Read from disk cache with TTL checking.
|
|
155
155
|
|
|
156
156
|
Args:
|
|
157
157
|
url: URL to read from cache
|
|
158
158
|
base_url: Base URL for service reference API
|
|
159
|
+
allow_stale: If True, return stale cache data even if expired
|
|
159
160
|
|
|
160
161
|
Returns:
|
|
161
|
-
Cached data if valid, None otherwise
|
|
162
|
+
Cached data if valid (or if allow_stale and data exists), None otherwise
|
|
162
163
|
"""
|
|
163
164
|
if not self.enable_cache:
|
|
164
165
|
return None
|
|
@@ -171,14 +172,21 @@ class ServiceFileStorage:
|
|
|
171
172
|
try:
|
|
172
173
|
# Check file modification time for TTL
|
|
173
174
|
mtime = cache_path.stat().st_mtime
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
is_expired = time.time() - mtime > self.cache_ttl
|
|
176
|
+
|
|
177
|
+
if is_expired and not allow_stale:
|
|
178
|
+
# Cache expired - don't delete the file, just return None
|
|
179
|
+
# The file will be refreshed (overwritten) on next successful fetch
|
|
180
|
+
logger.debug(f"Cache expired for {url} (keeping file for refresh)")
|
|
177
181
|
return None
|
|
178
182
|
|
|
179
183
|
with open(cache_path, encoding="utf-8") as f:
|
|
180
184
|
data = json.load(f)
|
|
181
|
-
|
|
185
|
+
|
|
186
|
+
if is_expired:
|
|
187
|
+
logger.debug(f"Disk cache stale hit for {url} (allow_stale=True)")
|
|
188
|
+
else:
|
|
189
|
+
logger.debug(f"Disk cache hit for {url}")
|
|
182
190
|
return data
|
|
183
191
|
|
|
184
192
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
@@ -13,7 +13,6 @@ from iam_validator.core.aws_service.parsers import ServiceParser
|
|
|
13
13
|
from iam_validator.core.constants import (
|
|
14
14
|
AWS_TAG_KEY_ALLOWED_CHARS,
|
|
15
15
|
AWS_TAG_KEY_MAX_LENGTH,
|
|
16
|
-
AWS_TAG_KEY_PLACEHOLDERS,
|
|
17
16
|
)
|
|
18
17
|
from iam_validator.core.models import ServiceDetail
|
|
19
18
|
|
|
@@ -46,42 +45,17 @@ def _is_valid_tag_key(tag_key: str) -> bool:
|
|
|
46
45
|
return bool(_TAG_KEY_PATTERN.match(tag_key))
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
def
|
|
50
|
-
"""Check if a condition key matches
|
|
48
|
+
def condition_key_in_list(condition_key: str, condition_keys: list[str]) -> bool:
|
|
49
|
+
"""Check if a condition key matches any key in the list, supporting patterns.
|
|
51
50
|
|
|
52
|
-
AWS service definitions use patterns like:
|
|
53
|
-
- `ssm:resourceTag/tag-key`
|
|
51
|
+
AWS service definitions use patterns with tag-key placeholders like:
|
|
52
|
+
- `ssm:resourceTag/tag-key` to match `ssm:resourceTag/owner`
|
|
54
53
|
- `aws:ResourceTag/${TagKey}` to match `aws:ResourceTag/Environment`
|
|
54
|
+
- `s3:RequestObjectTag/<key>` to match `s3:RequestObjectTag/Environment`
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
True if condition_key matches the pattern
|
|
62
|
-
"""
|
|
63
|
-
# Exact match (fast path)
|
|
64
|
-
if condition_key == pattern:
|
|
65
|
-
return True
|
|
66
|
-
|
|
67
|
-
# Check for tag-key placeholder patterns
|
|
68
|
-
for tag_placeholder in AWS_TAG_KEY_PLACEHOLDERS:
|
|
69
|
-
if tag_placeholder in pattern:
|
|
70
|
-
# Extract the prefix before the placeholder
|
|
71
|
-
prefix = pattern.split(tag_placeholder, 1)[0]
|
|
72
|
-
prefix_with_slash = prefix + "/"
|
|
73
|
-
# Check if condition_key starts with prefix and has a tag key after it
|
|
74
|
-
if condition_key.startswith(prefix_with_slash):
|
|
75
|
-
# Validate tag key format per AWS constraints
|
|
76
|
-
tag_key = condition_key[len(prefix_with_slash) :]
|
|
77
|
-
if _is_valid_tag_key(tag_key):
|
|
78
|
-
return True
|
|
79
|
-
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _condition_key_in_list(condition_key: str, condition_keys: list[str]) -> bool:
|
|
84
|
-
"""Check if a condition key matches any key in the list, supporting patterns.
|
|
56
|
+
Any pattern containing "/" is treated as a potential tag-key pattern where
|
|
57
|
+
the prefix before "/" must match exactly and the suffix after "/" in the
|
|
58
|
+
condition_key must be a valid AWS tag key.
|
|
85
59
|
|
|
86
60
|
Args:
|
|
87
61
|
condition_key: The condition key to check
|
|
@@ -94,13 +68,30 @@ def _condition_key_in_list(condition_key: str, condition_keys: list[str]) -> boo
|
|
|
94
68
|
if condition_key in condition_keys:
|
|
95
69
|
return True
|
|
96
70
|
|
|
97
|
-
#
|
|
71
|
+
# Check if condition_key could match a pattern (must contain "/")
|
|
72
|
+
if "/" not in condition_key:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Extract prefix and tag key from condition_key
|
|
76
|
+
cond_slash_idx = condition_key.rfind("/")
|
|
77
|
+
if cond_slash_idx <= 0:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
cond_prefix = condition_key[:cond_slash_idx]
|
|
81
|
+
tag_key = condition_key[cond_slash_idx + 1 :]
|
|
82
|
+
|
|
83
|
+
# Validate tag key format
|
|
84
|
+
if not _is_valid_tag_key(tag_key):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Check if any pattern has a matching prefix
|
|
98
88
|
for pattern in condition_keys:
|
|
99
|
-
|
|
100
|
-
if pattern == condition_key:
|
|
89
|
+
if "/" not in pattern:
|
|
101
90
|
continue
|
|
102
|
-
|
|
91
|
+
pattern_prefix = pattern[: pattern.rfind("/")]
|
|
92
|
+
if pattern_prefix == cond_prefix:
|
|
103
93
|
return True
|
|
94
|
+
|
|
104
95
|
return False
|
|
105
96
|
|
|
106
97
|
|
|
@@ -252,27 +243,35 @@ class ServiceValidator:
|
|
|
252
243
|
_, action_name = self._parser.parse_action(action)
|
|
253
244
|
|
|
254
245
|
# Check if it's a global condition key
|
|
246
|
+
# Note: Some aws: prefixed keys like aws:RequestTag/* and aws:ResourceTag/* are NOT
|
|
247
|
+
# global keys - they're action-specific or resource-specific. We'll check those later.
|
|
255
248
|
is_global_key = False
|
|
256
249
|
if condition_key.startswith("aws:"):
|
|
257
250
|
global_conditions = get_global_conditions()
|
|
258
251
|
if global_conditions.is_valid_global_key(condition_key):
|
|
259
252
|
is_global_key = True
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
is_valid=False,
|
|
263
|
-
error_message=f"Invalid AWS global condition key: `{condition_key}`.",
|
|
264
|
-
)
|
|
253
|
+
# If not a global key, continue to check action/resource-specific keys
|
|
254
|
+
# Don't return an error yet - aws:RequestTag, aws:ResourceTag are action-specific
|
|
265
255
|
|
|
266
256
|
# Check service-specific condition keys (with pattern matching for tag keys)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
257
|
+
# IMPORTANT: aws:RequestTag and aws:ResourceTag patterns in service-level keys
|
|
258
|
+
# are NOT universally valid for all actions. Skip them here - they'll be checked
|
|
259
|
+
# at action/resource level.
|
|
260
|
+
if service_detail.condition_keys:
|
|
261
|
+
# Check if it matches service-level keys, but exclude RequestTag/ResourceTag
|
|
262
|
+
if condition_key_in_list(condition_key, list(service_detail.condition_keys.keys())):
|
|
263
|
+
# If it's RequestTag or ResourceTag, don't return valid here - check action/resource level
|
|
264
|
+
if not (
|
|
265
|
+
condition_key.startswith("aws:RequestTag/")
|
|
266
|
+
or condition_key.startswith("aws:ResourceTag/")
|
|
267
|
+
):
|
|
268
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
269
|
+
# For RequestTag/ResourceTag, continue to check action/resource level
|
|
271
270
|
|
|
272
271
|
# Check action-specific condition keys
|
|
273
272
|
if action_name in service_detail.actions:
|
|
274
273
|
action_detail = service_detail.actions[action_name]
|
|
275
|
-
if action_detail.action_condition_keys and
|
|
274
|
+
if action_detail.action_condition_keys and condition_key_in_list(
|
|
276
275
|
condition_key, action_detail.action_condition_keys
|
|
277
276
|
):
|
|
278
277
|
return ConditionKeyValidationResult(is_valid=True)
|
|
@@ -288,7 +287,7 @@ class ServiceValidator:
|
|
|
288
287
|
# Look up resource type definition
|
|
289
288
|
resource_type = service_detail.resources.get(resource_name)
|
|
290
289
|
if resource_type and resource_type.condition_keys:
|
|
291
|
-
if
|
|
290
|
+
if condition_key_in_list(condition_key, resource_type.condition_keys):
|
|
292
291
|
return ConditionKeyValidationResult(is_valid=True)
|
|
293
292
|
|
|
294
293
|
# If it's a global key but the action has specific condition keys defined,
|
|
@@ -307,8 +306,26 @@ class ServiceValidator:
|
|
|
307
306
|
if is_global_key:
|
|
308
307
|
return ConditionKeyValidationResult(is_valid=True)
|
|
309
308
|
|
|
310
|
-
#
|
|
311
|
-
|
|
309
|
+
# If we reach here, the condition key was not found in any valid location
|
|
310
|
+
# Check if it's an aws: prefixed key that's not global - provide specific error
|
|
311
|
+
if condition_key.startswith("aws:"):
|
|
312
|
+
# Special handling for aws:RequestTag and aws:ResourceTag patterns
|
|
313
|
+
if condition_key.startswith("aws:RequestTag/"):
|
|
314
|
+
error_msg = (
|
|
315
|
+
f"Condition key `{condition_key}` is not supported by action `{action}`. "
|
|
316
|
+
f"The `aws:RequestTag/${{TagKey}}` condition is only supported by actions that "
|
|
317
|
+
f"create or modify resources with tags. This action does not support tag operations."
|
|
318
|
+
)
|
|
319
|
+
elif condition_key.startswith("aws:ResourceTag/"):
|
|
320
|
+
error_msg = (
|
|
321
|
+
f"Condition key `{condition_key}` is not supported by the resources used by action `{action}`. "
|
|
322
|
+
f"The `aws:ResourceTag/${{TagKey}}` condition is only supported by resources that have tags."
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
error_msg = f"Invalid AWS condition key: `{condition_key}`. This key is not a valid global condition key and is not supported by action `{action}`."
|
|
326
|
+
else:
|
|
327
|
+
# Short error message for non-aws: keys
|
|
328
|
+
error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
|
|
312
329
|
|
|
313
330
|
# Collect valid condition keys for this action
|
|
314
331
|
valid_keys: set[str] = set()
|