Skip to content

Commit a4e6cfc

Browse files
committed
adds working parallelisation
1 parent ce926df commit a4e6cfc

2 files changed

Lines changed: 60 additions & 4 deletions

File tree

iamspy/cli.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
from typing import List, Optional
34
import typer
45
from iamspy.model import Model
@@ -82,6 +83,7 @@ def who_can(
8283
strict_conditions: bool = typer.Option(
8384
False, help="Whether to require conditions to be passed in for any IAM condition checks"
8485
),
86+
workers: int = typer.Option(os.cpu_count(), "-w", help="Number of parallel worker processes"),
8587
model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f"),
8688
):
8789
"""
@@ -91,7 +93,7 @@ def who_can(
9193
if Path(model).is_file():
9294
m.load_model(model)
9395

94-
for x in m.who_can(action, resource, conditions, condition_file, strict_conditions):
96+
for x in m.who_can(action, resource, conditions, condition_file, strict_conditions, workers):
9597
print(x)
9698

9799

@@ -134,6 +136,7 @@ def who_can_batch_resource(
134136
strict_conditions: bool = typer.Option(
135137
False, help="Whether to require conditions to be passed in for any IAM condition checks"
136138
),
139+
workers: int = typer.Option(os.cpu_count(), "-w", help="Number of parallel worker processes"),
137140
model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f"),
138141
):
139142
"""
@@ -146,7 +149,7 @@ def who_can_batch_resource(
146149
with open(resources_file) as fs:
147150
resources = fs.read().strip().split("\n")
148151

149-
for source, resource in m.who_can_batch_resource(action, resources, conditions, condition_file, strict_conditions):
152+
for source, resource in m.who_can_batch_resource(action, resources, conditions, condition_file, strict_conditions, workers):
150153
print(f"{source} can perform {action} on {resource}")
151154

152155

iamspy/model.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import List, Optional, Set, Union, Tuple
22
import logging
33
import json
4+
import os
5+
import multiprocessing as mp
46
import z3
57
import itertools
68
from dataclasses import asdict
@@ -14,6 +16,39 @@
1416

1517
logger = logging.getLogger("iamspy.model")
1618

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+
1752

1853
class Model:
1954
def __init__(self):
@@ -340,21 +375,30 @@ def who_can(
340375
conditions: List[str] = [],
341376
condition_file: Optional[str] = None,
342377
strict_conditions: bool = False,
378+
workers: int = 1,
343379
) -> list[str]:
344380
"""
345381
Used by the CLI to provide the who-can call.
346382
"""
347383
possible_accounts = self._check_viable_source_accounts(action, resource)
348384

385+
sources = set()
349386
for gaad in self._model.gaads.values():
350387
if gaad.account not in possible_accounts:
351388
logger.debug(f"Skipping {gaad.account} GAAD as not a viable source account for {resource}")
352389
continue
353390
logger.debug(f"Checking identities in {gaad.account} GAAD")
354-
sources = set()
355391
for identity in gaad.RoleDetailList + gaad.UserDetailList:
356392
sources.add(identity.Arn)
357393

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:
358402
for source in sources:
359403
if self.can_i(
360404
source=source,
@@ -398,22 +442,31 @@ def who_can_batch_resource(
398442
conditions: List[str] = [],
399443
condition_file: Optional[str] = None,
400444
strict_conditions: bool = False,
445+
workers: int = 1,
401446
) -> List[Tuple[str, str]]:
402447
possible_accounts = set()
403448

404449
for resource in resources:
405450
accounts = self._check_viable_source_accounts(action, resource)
406451
possible_accounts.update(accounts)
407452

453+
sources = set()
408454
for gaad in self._model.gaads.values():
409455
if gaad.account not in possible_accounts:
410456
logger.debug(f"Skipping {gaad.account} GAAD as not a viable source account for {resource}")
411457
continue
412458
logger.debug(f"Checking identities in {gaad.account} GAAD")
413-
sources = set()
414459
for identity in gaad.RoleDetailList + gaad.UserDetailList:
415460
sources.add(identity.Arn)
416461

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:
417470
for source, resource in itertools.product(sources, resources):
418471
if self.can_i(
419472
source=source,

0 commit comments

Comments
 (0)