|
1 | 1 | from typing import List, Optional, Set, Union, Tuple |
2 | 2 | import logging |
3 | 3 | import json |
| 4 | +import os |
| 5 | +import multiprocessing as mp |
4 | 6 | import z3 |
5 | 7 | import itertools |
6 | 8 | from dataclasses import asdict |
|
14 | 16 |
|
15 | 17 | logger = logging.getLogger("iamspy.model") |
16 | 18 |
|
| 19 | +# Module-level worker state and functions — must be at module level to be picklable |
| 20 | +_worker_model = None |
| 21 | + |
| 22 | + |
| 23 | +def _init_worker(model_json: str): |
| 24 | + global _worker_model |
| 25 | + _worker_model = Model() |
| 26 | + _worker_model._model = DataModel(**json.loads(model_json)) |
| 27 | + |
| 28 | + |
| 29 | +def _check_source(args): |
| 30 | + source, action, resource, conditions, condition_file, strict_conditions = args |
| 31 | + return source if _worker_model.can_i( |
| 32 | + source=source, |
| 33 | + action=action, |
| 34 | + resource=resource, |
| 35 | + conditions=conditions, |
| 36 | + condition_file=condition_file, |
| 37 | + strict_conditions=strict_conditions, |
| 38 | + ) else None |
| 39 | + |
| 40 | + |
| 41 | +def _check_source_resource(args): |
| 42 | + source, action, resource, conditions, condition_file, strict_conditions = args |
| 43 | + return (source, resource) if _worker_model.can_i( |
| 44 | + source=source, |
| 45 | + action=action, |
| 46 | + resource=resource, |
| 47 | + conditions=conditions, |
| 48 | + condition_file=condition_file, |
| 49 | + strict_conditions=strict_conditions, |
| 50 | + ) else None |
| 51 | + |
17 | 52 |
|
18 | 53 | class Model: |
19 | 54 | def __init__(self): |
@@ -340,21 +375,30 @@ def who_can( |
340 | 375 | conditions: List[str] = [], |
341 | 376 | condition_file: Optional[str] = None, |
342 | 377 | strict_conditions: bool = False, |
| 378 | + workers: int = 1, |
343 | 379 | ) -> list[str]: |
344 | 380 | """ |
345 | 381 | Used by the CLI to provide the who-can call. |
346 | 382 | """ |
347 | 383 | possible_accounts = self._check_viable_source_accounts(action, resource) |
348 | 384 |
|
| 385 | + sources = set() |
349 | 386 | for gaad in self._model.gaads.values(): |
350 | 387 | if gaad.account not in possible_accounts: |
351 | 388 | logger.debug(f"Skipping {gaad.account} GAAD as not a viable source account for {resource}") |
352 | 389 | continue |
353 | 390 | logger.debug(f"Checking identities in {gaad.account} GAAD") |
354 | | - sources = set() |
355 | 391 | for identity in gaad.RoleDetailList + gaad.UserDetailList: |
356 | 392 | sources.add(identity.Arn) |
357 | 393 |
|
| 394 | + if workers > 1: |
| 395 | + model_json = json.dumps(asdict(self._model), default=json_serial) |
| 396 | + args = [(s, action, resource, conditions, condition_file, strict_conditions) for s in sources] |
| 397 | + with mp.Pool(workers, initializer=_init_worker, initargs=(model_json,)) as pool: |
| 398 | + for result in pool.map(_check_source, args): |
| 399 | + if result: |
| 400 | + yield result |
| 401 | + else: |
358 | 402 | for source in sources: |
359 | 403 | if self.can_i( |
360 | 404 | source=source, |
@@ -398,22 +442,31 @@ def who_can_batch_resource( |
398 | 442 | conditions: List[str] = [], |
399 | 443 | condition_file: Optional[str] = None, |
400 | 444 | strict_conditions: bool = False, |
| 445 | + workers: int = 1, |
401 | 446 | ) -> List[Tuple[str, str]]: |
402 | 447 | possible_accounts = set() |
403 | 448 |
|
404 | 449 | for resource in resources: |
405 | 450 | accounts = self._check_viable_source_accounts(action, resource) |
406 | 451 | possible_accounts.update(accounts) |
407 | 452 |
|
| 453 | + sources = set() |
408 | 454 | for gaad in self._model.gaads.values(): |
409 | 455 | if gaad.account not in possible_accounts: |
410 | 456 | logger.debug(f"Skipping {gaad.account} GAAD as not a viable source account for {resource}") |
411 | 457 | continue |
412 | 458 | logger.debug(f"Checking identities in {gaad.account} GAAD") |
413 | | - sources = set() |
414 | 459 | for identity in gaad.RoleDetailList + gaad.UserDetailList: |
415 | 460 | sources.add(identity.Arn) |
416 | 461 |
|
| 462 | + if workers > 1: |
| 463 | + model_json = json.dumps(asdict(self._model), default=json_serial) |
| 464 | + args = [(s, action, r, conditions, condition_file, strict_conditions) for s, r in itertools.product(sources, resources)] |
| 465 | + with mp.Pool(workers, initializer=_init_worker, initargs=(model_json,)) as pool: |
| 466 | + for result in pool.map(_check_source_resource, args): |
| 467 | + if result: |
| 468 | + yield result |
| 469 | + else: |
417 | 470 | for source, resource in itertools.product(sources, resources): |
418 | 471 | if self.can_i( |
419 | 472 | source=source, |
|
0 commit comments