iam-policy-validator 1.10.3__py3-none-any.whl → 1.11.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.
@@ -0,0 +1,454 @@
1
+ """Query utilities for AWS service definitions.
2
+
3
+ This module provides utilities for querying AWS IAM service metadata including
4
+ actions, ARN formats, and condition keys. These utilities are inspired by and
5
+ compatible with policy_sentry's query functionality.
6
+
7
+ Example:
8
+ Query actions for a service:
9
+ >>> async with AWSServiceFetcher() as fetcher:
10
+ ... actions = await query_actions(fetcher, "s3")
11
+ ... write_actions = await query_actions(fetcher, "s3", access_level="write")
12
+ ...
13
+ Query ARN formats:
14
+ >>> async with AWSServiceFetcher() as fetcher:
15
+ ... arns = await query_arn_formats(fetcher, "s3")
16
+ ... bucket_arn = await query_arn_format(fetcher, "s3", "bucket")
17
+ ...
18
+ Query condition keys:
19
+ >>> async with AWSServiceFetcher() as fetcher:
20
+ ... keys = await query_condition_keys(fetcher, "s3")
21
+ ... prefix_key = await query_condition_key(fetcher, "s3", "s3:prefix")
22
+ """
23
+
24
+ from typing import Any, Literal
25
+
26
+ from iam_validator.core.aws_service.fetcher import AWSServiceFetcher
27
+
28
+ AccessLevel = Literal["read", "write", "list", "tagging", "permissions-management"]
29
+
30
+
31
+ def _get_access_level(action_detail: Any) -> str:
32
+ """Derive access level from action annotations.
33
+
34
+ AWS API provides Properties dict with boolean flags instead of AccessLevel string.
35
+ We derive the access level from these flags.
36
+
37
+ Args:
38
+ action_detail: Action detail object with annotations
39
+
40
+ Returns:
41
+ Access level string: "permissions-management", "tagging", "write", "list", or "read"
42
+ """
43
+ if not action_detail.annotations:
44
+ return "Unknown"
45
+
46
+ props = action_detail.annotations.get("Properties", {})
47
+ if not props:
48
+ return "Unknown"
49
+
50
+ # Check flags in priority order
51
+ if props.get("IsPermissionManagement"):
52
+ return "permissions-management"
53
+ if props.get("IsTaggingOnly"):
54
+ return "tagging"
55
+ if props.get("IsWrite"):
56
+ return "write"
57
+ if props.get("IsList"):
58
+ return "list"
59
+
60
+ # Default to read if none of the above
61
+ return "read"
62
+
63
+
64
+ async def query_actions(
65
+ fetcher: AWSServiceFetcher,
66
+ service: str,
67
+ access_level: AccessLevel | None = None,
68
+ resource_type: str | None = None,
69
+ condition: str | None = None,
70
+ ) -> list[dict[str, Any]]:
71
+ """Query IAM actions for a service with optional filtering.
72
+
73
+ Args:
74
+ fetcher: AWSServiceFetcher instance
75
+ service: AWS service prefix (e.g., "s3", "iam", "ec2")
76
+ access_level: Optional filter by access level
77
+ resource_type: Optional filter by resource type. Use "*" for wildcard-only actions
78
+ condition: Optional filter by condition key support
79
+
80
+ Returns:
81
+ List of action dictionaries with keys: action, access_level, description
82
+
83
+ Example:
84
+ >>> async with AWSServiceFetcher() as fetcher:
85
+ ... # Get all S3 actions
86
+ ... all_actions = await query_actions(fetcher, "s3")
87
+ ...
88
+ ... # Get only write-level S3 actions
89
+ ... write_actions = await query_actions(fetcher, "s3", access_level="write")
90
+ ...
91
+ ... # Get wildcard-only actions (no resource constraint)
92
+ ... wildcard_actions = await query_actions(fetcher, "iam", resource_type="*")
93
+ ...
94
+ ... # Get actions supporting specific condition key
95
+ ... mfa_actions = await query_actions(fetcher, "iam", condition="aws:MultiFactorAuthPresent")
96
+ """
97
+ service_detail = await fetcher.fetch_service_by_name(service)
98
+
99
+ filtered_actions = []
100
+ for action_name, action_detail in service_detail.actions.items():
101
+ access_lv = _get_access_level(action_detail)
102
+
103
+ # Apply filters
104
+ if access_level and access_lv.lower() != access_level.lower():
105
+ continue
106
+
107
+ if resource_type:
108
+ resources = action_detail.resources or []
109
+
110
+ # If filtering for wildcard-only actions (actions with no required resources)
111
+ if resource_type == "*":
112
+ # Actions with empty resources list are wildcard-only
113
+ if resources:
114
+ continue
115
+ else:
116
+ # Filter by specific resource type name
117
+ resource_names = [r.get("Name", "") for r in resources]
118
+ if resource_type not in resource_names:
119
+ continue
120
+
121
+ if condition:
122
+ condition_keys = action_detail.action_condition_keys or []
123
+ if condition not in condition_keys:
124
+ continue
125
+
126
+ description = (
127
+ action_detail.annotations.get("Description", "N/A")
128
+ if action_detail.annotations
129
+ else "N/A"
130
+ )
131
+
132
+ filtered_actions.append(
133
+ {
134
+ "action": f"{service}:{action_name}",
135
+ "access_level": access_lv,
136
+ "description": description,
137
+ }
138
+ )
139
+
140
+ return filtered_actions
141
+
142
+
143
+ async def query_action_details(
144
+ fetcher: AWSServiceFetcher,
145
+ service: str,
146
+ action_name: str,
147
+ ) -> dict[str, Any]:
148
+ """Get detailed information about a specific action.
149
+
150
+ Args:
151
+ fetcher: AWSServiceFetcher instance
152
+ service: AWS service prefix (e.g., "s3", "iam")
153
+ action_name: Action name (e.g., "GetObject", "CreateUser")
154
+
155
+ Returns:
156
+ Dictionary with action details including resource types and condition keys
157
+
158
+ Raises:
159
+ ValueError: If action is not found
160
+
161
+ Example:
162
+ >>> async with AWSServiceFetcher() as fetcher:
163
+ ... details = await query_action_details(fetcher, "s3", "GetObject")
164
+ ... print(f"Access level: {details['access_level']}")
165
+ ... print(f"Resource types: {details['resource_types']}")
166
+ """
167
+ service_detail = await fetcher.fetch_service_by_name(service)
168
+
169
+ # Try case-insensitive lookup
170
+ action_detail = None
171
+ for key, detail in service_detail.actions.items():
172
+ if key.lower() == action_name.lower():
173
+ action_detail = detail
174
+ break
175
+
176
+ if not action_detail:
177
+ raise ValueError(f"Action '{action_name}' not found in service '{service}'")
178
+
179
+ access_level = _get_access_level(action_detail)
180
+ description = (
181
+ action_detail.annotations.get("Description", "N/A") if action_detail.annotations else "N/A"
182
+ )
183
+
184
+ return {
185
+ "service": service,
186
+ "action": action_detail.name,
187
+ "description": description,
188
+ "access_level": access_level,
189
+ "resource_types": [r.get("Name", "*") for r in (action_detail.resources or [])],
190
+ "condition_keys": action_detail.action_condition_keys or [],
191
+ }
192
+
193
+
194
+ async def query_arn_formats(
195
+ fetcher: AWSServiceFetcher,
196
+ service: str,
197
+ ) -> list[str]:
198
+ """Query all ARN formats for a service.
199
+
200
+ Args:
201
+ fetcher: AWSServiceFetcher instance
202
+ service: AWS service prefix (e.g., "s3", "iam")
203
+
204
+ Returns:
205
+ List of unique ARN format strings
206
+
207
+ Example:
208
+ >>> async with AWSServiceFetcher() as fetcher:
209
+ ... arns = await query_arn_formats(fetcher, "s3")
210
+ ... for arn in arns:
211
+ ... print(arn)
212
+ """
213
+ service_detail = await fetcher.fetch_service_by_name(service)
214
+
215
+ all_arns = []
216
+ for resource_type in service_detail.resources.values():
217
+ if resource_type.arn_formats:
218
+ all_arns.extend(resource_type.arn_formats)
219
+
220
+ return list(set(all_arns)) # Remove duplicates
221
+
222
+
223
+ async def query_arn_types(
224
+ fetcher: AWSServiceFetcher,
225
+ service: str,
226
+ ) -> list[dict[str, Any]]:
227
+ """Query all ARN resource types with their formats.
228
+
229
+ Args:
230
+ fetcher: AWSServiceFetcher instance
231
+ service: AWS service prefix (e.g., "s3", "iam")
232
+
233
+ Returns:
234
+ List of dictionaries with resource_type and arn_formats keys
235
+
236
+ Example:
237
+ >>> async with AWSServiceFetcher() as fetcher:
238
+ ... types = await query_arn_types(fetcher, "s3")
239
+ ... for rt in types:
240
+ ... print(f"{rt['resource_type']}: {rt['arn_formats']}")
241
+ """
242
+ service_detail = await fetcher.fetch_service_by_name(service)
243
+
244
+ return [
245
+ {
246
+ "resource_type": rt.name,
247
+ "arn_formats": rt.arn_formats or [],
248
+ }
249
+ for rt in service_detail.resources.values()
250
+ ]
251
+
252
+
253
+ async def query_arn_format(
254
+ fetcher: AWSServiceFetcher,
255
+ service: str,
256
+ resource_type_name: str,
257
+ ) -> dict[str, Any]:
258
+ """Get ARN format details for a specific resource type.
259
+
260
+ Args:
261
+ fetcher: AWSServiceFetcher instance
262
+ service: AWS service prefix (e.g., "s3", "iam")
263
+ resource_type_name: Resource type name (e.g., "bucket", "role")
264
+
265
+ Returns:
266
+ Dictionary with resource type details including ARN formats and condition keys
267
+
268
+ Raises:
269
+ ValueError: If resource type is not found
270
+
271
+ Example:
272
+ >>> async with AWSServiceFetcher() as fetcher:
273
+ ... details = await query_arn_format(fetcher, "s3", "bucket")
274
+ ... print(f"ARN formats: {details['arn_formats']}")
275
+ """
276
+ service_detail = await fetcher.fetch_service_by_name(service)
277
+
278
+ resource_type = None
279
+ for key, rt in service_detail.resources.items():
280
+ if key.lower() == resource_type_name.lower():
281
+ resource_type = rt
282
+ break
283
+
284
+ if not resource_type:
285
+ raise ValueError(
286
+ f"ARN resource type '{resource_type_name}' not found in service '{service}'"
287
+ )
288
+
289
+ return {
290
+ "service": service,
291
+ "resource_type": resource_type.name,
292
+ "arn_formats": resource_type.arn_formats or [],
293
+ "condition_keys": resource_type.condition_keys or [],
294
+ }
295
+
296
+
297
+ async def query_condition_keys(
298
+ fetcher: AWSServiceFetcher,
299
+ service: str,
300
+ ) -> list[dict[str, Any]]:
301
+ """Query all condition keys for a service.
302
+
303
+ Args:
304
+ fetcher: AWSServiceFetcher instance
305
+ service: AWS service prefix (e.g., "s3", "iam")
306
+
307
+ Returns:
308
+ List of dictionaries with condition_key, description, and types keys
309
+
310
+ Example:
311
+ >>> async with AWSServiceFetcher() as fetcher:
312
+ ... keys = await query_condition_keys(fetcher, "s3")
313
+ ... for key in keys:
314
+ ... print(f"{key['condition_key']}: {key['description']}")
315
+ """
316
+ service_detail = await fetcher.fetch_service_by_name(service)
317
+
318
+ return [
319
+ {
320
+ "condition_key": ck.name,
321
+ "description": ck.description or "N/A",
322
+ "types": ck.types or [],
323
+ }
324
+ for ck in service_detail.condition_keys.values()
325
+ ]
326
+
327
+
328
+ async def query_condition_key(
329
+ fetcher: AWSServiceFetcher,
330
+ service: str,
331
+ condition_key_name: str,
332
+ ) -> dict[str, Any]:
333
+ """Get details for a specific condition key.
334
+
335
+ Args:
336
+ fetcher: AWSServiceFetcher instance
337
+ service: AWS service prefix (e.g., "s3", "iam")
338
+ condition_key_name: Condition key name (e.g., "s3:prefix", "iam:PolicyArn")
339
+
340
+ Returns:
341
+ Dictionary with condition key details
342
+
343
+ Raises:
344
+ ValueError: If condition key is not found
345
+
346
+ Example:
347
+ >>> async with AWSServiceFetcher() as fetcher:
348
+ ... details = await query_condition_key(fetcher, "s3", "s3:prefix")
349
+ ... print(f"Types: {details['types']}")
350
+ """
351
+ service_detail = await fetcher.fetch_service_by_name(service)
352
+
353
+ condition_key = None
354
+ for key, ck in service_detail.condition_keys.items():
355
+ if key.lower() == condition_key_name.lower():
356
+ condition_key = ck
357
+ break
358
+
359
+ if not condition_key:
360
+ raise ValueError(f"Condition key '{condition_key_name}' not found in service '{service}'")
361
+
362
+ return {
363
+ "service": service,
364
+ "condition_key": condition_key.name,
365
+ "description": condition_key.description or "N/A",
366
+ "types": condition_key.types or [],
367
+ }
368
+
369
+
370
+ async def get_actions_by_access_level(
371
+ fetcher: AWSServiceFetcher,
372
+ service: str,
373
+ access_level: AccessLevel,
374
+ ) -> list[str]:
375
+ """Get action names filtered by access level.
376
+
377
+ Convenience function that returns just the action names (not full details).
378
+
379
+ Args:
380
+ fetcher: AWSServiceFetcher instance
381
+ service: AWS service prefix
382
+ access_level: Access level to filter by
383
+
384
+ Returns:
385
+ List of action names (with service prefix)
386
+
387
+ Example:
388
+ >>> async with AWSServiceFetcher() as fetcher:
389
+ ... write_actions = await get_actions_by_access_level(fetcher, "s3", "write")
390
+ ... print(f"Found {len(write_actions)} write actions")
391
+ """
392
+ actions = await query_actions(fetcher, service, access_level=access_level)
393
+ return [action["action"] for action in actions]
394
+
395
+
396
+ async def get_wildcard_only_actions(
397
+ fetcher: AWSServiceFetcher,
398
+ service: str,
399
+ ) -> list[str]:
400
+ """Get actions that only support wildcard resources (no specific resource types).
401
+
402
+ Args:
403
+ fetcher: AWSServiceFetcher instance
404
+ service: AWS service prefix
405
+
406
+ Returns:
407
+ List of action names that don't require specific resource ARNs
408
+
409
+ Example:
410
+ >>> async with AWSServiceFetcher() as fetcher:
411
+ ... wildcard_actions = await get_wildcard_only_actions(fetcher, "iam")
412
+ ... print(f"IAM has {len(wildcard_actions)} wildcard-only actions")
413
+ """
414
+ actions = await query_actions(fetcher, service, resource_type="*")
415
+ return [action["action"] for action in actions]
416
+
417
+
418
+ async def get_actions_supporting_condition(
419
+ fetcher: AWSServiceFetcher,
420
+ service: str,
421
+ condition_key: str,
422
+ ) -> list[str]:
423
+ """Get actions that support a specific condition key.
424
+
425
+ Args:
426
+ fetcher: AWSServiceFetcher instance
427
+ service: AWS service prefix
428
+ condition_key: Condition key to search for
429
+
430
+ Returns:
431
+ List of action names that support the condition key
432
+
433
+ Example:
434
+ >>> async with AWSServiceFetcher() as fetcher:
435
+ ... mfa_actions = await get_actions_supporting_condition(
436
+ ... fetcher, "iam", "aws:MultiFactorAuthPresent"
437
+ ... )
438
+ """
439
+ actions = await query_actions(fetcher, service, condition=condition_key)
440
+ return [action["action"] for action in actions]
441
+
442
+
443
+ __all__ = [
444
+ "query_actions",
445
+ "query_action_details",
446
+ "query_arn_formats",
447
+ "query_arn_types",
448
+ "query_arn_format",
449
+ "query_condition_keys",
450
+ "query_condition_key",
451
+ "get_actions_by_access_level",
452
+ "get_wildcard_only_actions",
453
+ "get_actions_supporting_condition",
454
+ ]