grantry 0.1.0__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.
- grantry/__init__.py +3 -0
- grantry/__main__.py +6 -0
- grantry/admin.py +125 -0
- grantry/assignments_graph_template.html +710 -0
- grantry/audit.py +42 -0
- grantry/awscli_cache.py +43 -0
- grantry/broker.py +248 -0
- grantry/cli.py +593 -0
- grantry/config.py +17 -0
- grantry/graphdata.py +60 -0
- grantry/humanops.py +139 -0
- grantry/identity.py +21 -0
- grantry/instance.py +98 -0
- grantry/logging_setup.py +39 -0
- grantry/mcp_install.py +92 -0
- grantry/mcp_server.py +140 -0
- grantry/policy.py +104 -0
- grantry/providers/__init__.py +0 -0
- grantry/providers/aws.py +165 -0
- grantry/providers/base.py +46 -0
- grantry/render.py +196 -0
- grantry/scaffold.py +53 -0
- grantry/secrets.py +25 -0
- grantry/ttl.py +23 -0
- grantry-0.1.0.dist-info/METADATA +212 -0
- grantry-0.1.0.dist-info/RECORD +29 -0
- grantry-0.1.0.dist-info/WHEEL +4 -0
- grantry-0.1.0.dist-info/entry_points.txt +2 -0
- grantry-0.1.0.dist-info/licenses/LICENSE +202 -0
grantry/__init__.py
ADDED
grantry/__main__.py
ADDED
grantry/admin.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Org-wide assignment crawl: who has which permission set in which account.
|
|
2
|
+
|
|
3
|
+
This is admin-only by construction. It calls the sso-admin, organizations, and
|
|
4
|
+
identitystore APIs, which require signed credentials from an account that holds
|
|
5
|
+
those permissions (typically the management or a delegated-admin account).
|
|
6
|
+
grantry mints those credentials through the normal policy path, so a caller who
|
|
7
|
+
cannot assume such a role simply gets nothing; AWS is the gatekeeper.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable, Iterator
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
ClientFactory = Callable[[str], Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Assignment:
|
|
21
|
+
principal_type: str
|
|
22
|
+
principal_id: str
|
|
23
|
+
principal_name: str
|
|
24
|
+
permission_set_name: str
|
|
25
|
+
account_id: str
|
|
26
|
+
account_name: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _paginate(op: Callable[..., dict[str, Any]], key: str, **kwargs: Any) -> Iterator[Any]:
|
|
30
|
+
token: str | None = None
|
|
31
|
+
while True:
|
|
32
|
+
call = dict(kwargs)
|
|
33
|
+
if token:
|
|
34
|
+
call["NextToken"] = token
|
|
35
|
+
resp = op(**call)
|
|
36
|
+
yield from resp.get(key, [])
|
|
37
|
+
token = resp.get("NextToken")
|
|
38
|
+
if not token:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _principal_name(
|
|
43
|
+
idstore: Any,
|
|
44
|
+
identity_store_id: str,
|
|
45
|
+
ptype: str,
|
|
46
|
+
pid: str,
|
|
47
|
+
cache: dict[tuple[str, str], str],
|
|
48
|
+
) -> str:
|
|
49
|
+
# A group or user appears on many assignments; resolve each unique principal
|
|
50
|
+
# ONCE and cache it. At 10k+ assignments this turns tens of thousands of
|
|
51
|
+
# describe calls into one per distinct principal, which is the difference
|
|
52
|
+
# between a crawl that finishes and one that gets throttled.
|
|
53
|
+
key = (ptype, pid)
|
|
54
|
+
if key in cache:
|
|
55
|
+
return cache[key]
|
|
56
|
+
try:
|
|
57
|
+
if ptype == "GROUP":
|
|
58
|
+
resp = idstore.describe_group(IdentityStoreId=identity_store_id, GroupId=pid)
|
|
59
|
+
name = str(resp.get("DisplayName", pid))
|
|
60
|
+
else:
|
|
61
|
+
resp = idstore.describe_user(IdentityStoreId=identity_store_id, UserId=pid)
|
|
62
|
+
name = str(resp.get("UserName", pid))
|
|
63
|
+
except Exception:
|
|
64
|
+
name = pid
|
|
65
|
+
cache[key] = name
|
|
66
|
+
return name
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def crawl_assignments(
|
|
70
|
+
make_client: ClientFactory,
|
|
71
|
+
on_progress: Callable[[int, int], None] | None = None,
|
|
72
|
+
) -> list[Assignment]:
|
|
73
|
+
sso = make_client("sso-admin")
|
|
74
|
+
idstore = make_client("identitystore")
|
|
75
|
+
orgs = make_client("organizations")
|
|
76
|
+
|
|
77
|
+
instances = sso.list_instances().get("Instances", [])
|
|
78
|
+
if not instances:
|
|
79
|
+
return []
|
|
80
|
+
instance_arn = instances[0]["InstanceArn"]
|
|
81
|
+
identity_store_id = instances[0]["IdentityStoreId"]
|
|
82
|
+
|
|
83
|
+
accounts: dict[str, str] = {}
|
|
84
|
+
for acct in _paginate(orgs.list_accounts, "Accounts"):
|
|
85
|
+
accounts[acct["Id"]] = acct.get("Name", acct["Id"])
|
|
86
|
+
|
|
87
|
+
ps_names: dict[str, str] = {}
|
|
88
|
+
for ps_arn in _paginate(sso.list_permission_sets, "PermissionSets", InstanceArn=instance_arn):
|
|
89
|
+
d = sso.describe_permission_set(InstanceArn=instance_arn, PermissionSetArn=ps_arn)
|
|
90
|
+
ps_names[ps_arn] = d["PermissionSet"]["Name"]
|
|
91
|
+
|
|
92
|
+
out: list[Assignment] = []
|
|
93
|
+
name_cache: dict[tuple[str, str], str] = {}
|
|
94
|
+
total_accounts = len(accounts)
|
|
95
|
+
for done, (acct_id, acct_name) in enumerate(accounts.items(), start=1):
|
|
96
|
+
for ps_arn in _paginate(
|
|
97
|
+
sso.list_permission_sets_provisioned_to_account,
|
|
98
|
+
"PermissionSets",
|
|
99
|
+
InstanceArn=instance_arn,
|
|
100
|
+
AccountId=acct_id,
|
|
101
|
+
):
|
|
102
|
+
for a in _paginate(
|
|
103
|
+
sso.list_account_assignments,
|
|
104
|
+
"AccountAssignments",
|
|
105
|
+
InstanceArn=instance_arn,
|
|
106
|
+
AccountId=acct_id,
|
|
107
|
+
PermissionSetArn=ps_arn,
|
|
108
|
+
):
|
|
109
|
+
ptype = a["PrincipalType"]
|
|
110
|
+
pid = a["PrincipalId"]
|
|
111
|
+
out.append(
|
|
112
|
+
Assignment(
|
|
113
|
+
principal_type=ptype,
|
|
114
|
+
principal_id=pid,
|
|
115
|
+
principal_name=_principal_name(
|
|
116
|
+
idstore, identity_store_id, ptype, pid, name_cache
|
|
117
|
+
),
|
|
118
|
+
permission_set_name=ps_names.get(ps_arn, ps_arn),
|
|
119
|
+
account_id=acct_id,
|
|
120
|
+
account_name=acct_name,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
if on_progress:
|
|
124
|
+
on_progress(done, total_accounts)
|
|
125
|
+
return out
|