Skip to content

Commit 9e266ad

Browse files
author
AWS
committed
Release: 1.6.5
1 parent 49e23b6 commit 9e266ad

File tree

6 files changed

+157
-106
lines changed

6 files changed

+157
-106
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.6.4
1+
1.6.5

sources/aft-lambda-layer/aft_common/account_provisioning_framework.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,12 @@ def persist_metadata(
298298
def get_ssm_parameters_names_by_path(session: Session, path: str) -> List[str]:
299299

300300
client = session.client("ssm")
301-
response = client.get_parameters_by_path(Path=path, Recursive=True)
302-
logger.debug(response)
301+
paginator = client.get_paginator("get_parameters_by_path")
302+
pages = paginator.paginate(Path=path, Recursive=True)
303303

304304
parameter_names = []
305-
for p in response["Parameters"]:
306-
parameter_names.append(p["Name"])
305+
for page in pages:
306+
parameter_names.extend([param["Name"] for param in page["Parameters"]])
307307

308308
return parameter_names
309309

@@ -313,18 +313,16 @@ def delete_ssm_parameters(session: Session, parameters: Sequence[str]) -> None:
313313
if len(parameters) > 0:
314314
client = session.client("ssm")
315315
response = client.delete_parameters(Names=parameters)
316-
logger.info(response)
317316

318317

319-
def create_ssm_parameters(session: Session, parameters: Dict[str, str]) -> None:
318+
def put_ssm_parameters(session: Session, parameters: Dict[str, str]) -> None:
320319

321320
client = session.client("ssm")
322321

323322
for key, value in parameters.items():
324323
response = client.put_parameter(
325324
Name=SSM_PARAMETER_PATH + key, Value=value, Type="String", Overwrite=True
326325
)
327-
logger.info(response)
328326

329327

330328
def tag_account(

sources/aft-lambda-layer/aft_common/aft_utils.py

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -481,72 +481,6 @@ def get_all_aft_account_ids(session: Session) -> List[str]:
481481
return aft_account_ids
482482

483483

484-
def get_account_ids_in_ous(
485-
session: Session, ou_names: List[str]
486-
) -> Optional[List[str]]:
487-
client: OrganizationsClient = session.client("organizations")
488-
logger.info("Getting Account IDs in the following OUs: " + str(ou_names))
489-
ou_ids = []
490-
account_ids = []
491-
for n in ou_names:
492-
ou_ids.append(get_org_ou_id(session, n))
493-
logger.info("OU IDs: " + str(ou_ids))
494-
for ou_id in ou_ids:
495-
if ou_id is not None:
496-
logger.info("Listing accounts in the OU ID " + ou_id)
497-
498-
response = client.list_children(ParentId=ou_id, ChildType="ACCOUNT")
499-
children = response["Children"]
500-
while "NextToken" in response:
501-
response = client.list_children(
502-
ParentId=ou_id,
503-
ChildType="ACCOUNT",
504-
NextToken=response["NextToken"],
505-
)
506-
children.extend(response["Children"])
507-
508-
logger.info(str(children))
509-
510-
for a in children:
511-
account_ids.append(a["Id"])
512-
else:
513-
logger.info("OUs in " + str(ou_names) + " was not found")
514-
logger.info("Account IDs: " + str(account_ids))
515-
if len(account_ids) > 0:
516-
return account_ids
517-
else:
518-
return None
519-
520-
521-
def get_org_ou_id(session: Session, ou_name: str) -> Optional[str]:
522-
client: OrganizationsClient = session.client("organizations")
523-
logger.info("Listing Org Roots")
524-
list_roots_response = client.list_roots(MaxResults=1)
525-
logger.info(list_roots_response)
526-
root_id = list_roots_response["Roots"][0]["Id"]
527-
logger.info("Root ID is " + root_id)
528-
529-
logger.info("Listing OUs in the Organization")
530-
531-
list_ou_response = client.list_organizational_units_for_parent(ParentId=root_id)
532-
ous = list_ou_response["OrganizationalUnits"]
533-
while "NextToken" in list_ou_response:
534-
list_ou_response = client.list_organizational_units_for_parent(
535-
ParentId=root_id, NextToken=list_ou_response["NextToken"]
536-
)
537-
ous.extend(list_ou_response["OrganizationalUnits"])
538-
539-
logger.info(ous)
540-
541-
for ou in ous:
542-
if ou["Name"] == ou_name:
543-
ou_id: str = ou["Id"]
544-
logger.info("OU ID for " + ou_name + " is " + ou_id)
545-
return ou_id
546-
547-
return None
548-
549-
550484
def get_accounts_by_tags(
551485
aft_mgmt_session: Session, ct_mgmt_session: Session, tags: List[Dict[str, str]]
552486
) -> Optional[List[str]]:

sources/aft-lambda-layer/aft_common/customizations.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import aft_common.aft_utils as utils
1010
import jsonschema
11+
from aft_common.organizations import OrganizationsAgent
1112
from boto3.session import Session
1213

1314
CUSTOMIZATIONS_PIPELINE_PATTERN = "^\d\d\d\d\d\d\d\d\d\d\d\d-.*$"
@@ -201,11 +202,10 @@ def get_included_accounts(
201202
core_accounts = get_core_accounts(session)
202203
included_accounts.extend(core_accounts)
203204
if d["type"] == "ous":
204-
ou_accounts = utils.get_account_ids_in_ous(
205-
ct_mgmt_session, d["target_value"]
205+
orgs_agent = OrganizationsAgent(ct_mgmt_session)
206+
included_accounts.extend(
207+
orgs_agent.get_account_ids_in_ous(ou_names=d["target_value"])
206208
)
207-
if ou_accounts is not None:
208-
included_accounts.extend(ou_accounts)
209209
if d["type"] == "tags":
210210
tag_accounts = utils.get_accounts_by_tags(
211211
session, ct_mgmt_session, d["target_value"]
@@ -234,11 +234,10 @@ def get_excluded_accounts(
234234
core_accounts = get_core_accounts(session)
235235
excluded_accounts.extend(core_accounts)
236236
if d["type"] == "ous":
237-
ou_accounts = utils.get_account_ids_in_ous(
238-
ct_mgmt_session, d["target_value"]
237+
orgs_agent = OrganizationsAgent(ct_mgmt_session)
238+
excluded_accounts.extend(
239+
orgs_agent.get_account_ids_in_ous(ou_names=d["target_value"])
239240
)
240-
if ou_accounts is not None:
241-
excluded_accounts.extend(ou_accounts)
242241
if d["type"] == "tags":
243242
tag_accounts = utils.get_accounts_by_tags(
244243
session, ct_mgmt_session, d["target_value"]
Lines changed: 142 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
#
4-
from typing import TYPE_CHECKING, List, Optional
4+
import re
5+
from copy import deepcopy
6+
from typing import TYPE_CHECKING, List, Optional, Tuple
57

8+
from aft_common.aft_utils import get_logger
69
from boto3.session import Session
710

811
if TYPE_CHECKING:
@@ -21,45 +24,151 @@
2124
OrganizationalUnitTypeDef = object
2225
AccountTypeDef = object
2326

27+
logger = get_logger()
28+
2429

2530
class OrganizationsAgent:
2631
ROOT_OU = "Root"
32+
# https://docs.aws.amazon.com/organizations/latest/APIReference/API_OrganizationalUnit.html
33+
# Ex: Sandbox (ou-1234-zxcv)
34+
OU_ID_PATTERN = r"\(ou-.*\)"
35+
OU_NAME_PATTERN = r".{1,128}"
36+
NESTED_OU_NAME_PATTERN = (
37+
rf"{OU_NAME_PATTERN}\s{OU_ID_PATTERN}" # <Name> space (<Id>)
38+
)
2739

2840
def __init__(self, ct_management_session: Session):
2941
self.orgs_client: OrganizationsClient = ct_management_session.client(
3042
"organizations"
3143
)
3244

45+
# Memoize expensive all-org traversal
46+
self.org_ous: Optional[List[OrganizationalUnitTypeDef]] = None
47+
48+
@staticmethod
49+
def ou_name_is_nested_format(ou_name: str) -> bool:
50+
pattern = re.compile(OrganizationsAgent.NESTED_OU_NAME_PATTERN)
51+
if pattern.match(ou_name) is not None:
52+
return True
53+
return False
54+
55+
@staticmethod
56+
def get_name_and_id_from_nested_ou(
57+
nested_ou_name: str,
58+
) -> Optional[Tuple[str, str]]:
59+
if not OrganizationsAgent.ou_name_is_nested_format(ou_name=nested_ou_name):
60+
return None
61+
62+
pattern = re.compile(OrganizationsAgent.OU_ID_PATTERN)
63+
match = pattern.search(nested_ou_name)
64+
if match is None:
65+
return None
66+
first_id_idx, last_id_idx = match.span()
67+
68+
# Grab the matched ID from the nested-ou-string using the span,
69+
id = nested_ou_name[first_id_idx:last_id_idx]
70+
id = id.strip("()")
71+
72+
# The name is what remains of the nested OU without the ID, minus
73+
# the whitespace between the name and ID
74+
name = nested_ou_name[: first_id_idx - 1]
75+
return (name, id)
76+
3377
def get_root_ou_id(self) -> str:
3478
return self.orgs_client.list_roots()["Roots"][0]["Id"]
3579

3680
def get_ous_for_root(self) -> List[OrganizationalUnitTypeDef]:
81+
return self.get_children_ous_from_parent_id(parent_id=self.get_root_ou_id())
82+
83+
def get_all_org_ous(self) -> List[OrganizationalUnitTypeDef]:
84+
# Memoize calls / cache previous results
85+
# Cache is not shared between invocations so staleness due to org updates is unlikely
86+
if self.org_ous is not None:
87+
return self.org_ous
88+
89+
# Including the root OU
90+
list_root_response = self.orgs_client.list_roots()
91+
root_ou: OrganizationalUnitTypeDef = {
92+
"Id": list_root_response["Roots"][0]["Id"],
93+
"Arn": list_root_response["Roots"][0]["Arn"],
94+
"Name": list_root_response["Roots"][0]["Name"],
95+
}
96+
97+
org_ous = [root_ou]
98+
99+
# Get the children OUs of the root as the first pass
100+
root_children = self.get_ous_for_root()
101+
org_ous.extend(root_children)
102+
103+
# Exclude root to avoid double counting children
104+
ous_to_query = deepcopy(root_children)
105+
106+
# Recursively search all children OUs for further children
107+
while len(ous_to_query) > 0:
108+
parent_id: str = ous_to_query.pop()["Id"]
109+
children_ous = self.get_children_ous_from_parent_id(parent_id=parent_id)
110+
org_ous.extend(children_ous)
111+
ous_to_query.extend(children_ous)
112+
113+
self.org_ous = org_ous
114+
115+
return self.org_ous
116+
117+
def get_children_ous_from_parent_id(
118+
self, parent_id: str
119+
) -> List[OrganizationalUnitTypeDef]:
120+
37121
paginator = self.orgs_client.get_paginator(
38122
"list_organizational_units_for_parent"
39123
)
40-
pages = paginator.paginate(ParentId=self.get_root_ou_id())
41-
ous_under_root = []
124+
pages = paginator.paginate(ParentId=parent_id)
125+
children_ous = []
42126
for page in pages:
43-
ous_under_root.extend(page["OrganizationalUnits"])
44-
return ous_under_root
127+
children_ous.extend(page["OrganizationalUnits"])
128+
return children_ous
45129

46-
def list_tags_for_resource(self, resource: str) -> List[TagTypeDef]:
47-
return self.orgs_client.list_tags_for_resource(ResourceId=resource)["Tags"]
130+
def get_ou_ids_from_ou_names(self, target_ou_names: List[str]) -> List[str]:
131+
ous = self.get_all_org_ous()
132+
ou_map = {}
133+
134+
# Convert list of OUs to name->id map for constant time lookups
135+
for ou in ous:
136+
ou_map[ou["Name"]] = ou["Id"]
48137

49-
def get_ou_id_for_account_id(
138+
# Search the map for every target exactly once
139+
matched_ou_ids = []
140+
for target_name in target_ou_names:
141+
# Only match nested OU targets if both name and ID are the same
142+
nested_parsed = OrganizationsAgent.get_name_and_id_from_nested_ou(
143+
nested_ou_name=target_name
144+
)
145+
if nested_parsed is not None: # Nested OU pattern matched!
146+
target_name, target_id = nested_parsed
147+
if ou_map[target_name] == target_id:
148+
matched_ou_ids.append(ou_map[target_name])
149+
else:
150+
if target_name in ou_map:
151+
matched_ou_ids.append(ou_map[target_name])
152+
153+
return matched_ou_ids
154+
155+
def get_ou_from_account_id(
50156
self, account_id: str
51-
) -> Optional[DescribeOrganizationalUnitResponseTypeDef]:
157+
) -> Optional[OrganizationalUnitTypeDef]:
52158
if self.account_id_is_member_of_root(account_id=account_id):
53-
return self.orgs_client.describe_organizational_unit(
54-
OrganizationalUnitId=self.get_root_ou_id()
55-
)
56-
ou_ids = [ou["Id"] for ou in self.get_ous_for_root()]
57-
for ou_id in ou_ids:
58-
ou_accounts = self.get_accounts_for_ou(ou_id=ou_id)
59-
if account_id in [account_object["Id"] for account_object in ou_accounts]:
60-
return self.orgs_client.describe_organizational_unit(
61-
OrganizationalUnitId=ou_id
62-
)
159+
list_root_response = self.orgs_client.list_roots()
160+
root_ou: OrganizationalUnitTypeDef = {
161+
"Id": list_root_response["Roots"][0]["Id"],
162+
"Arn": list_root_response["Roots"][0]["Arn"],
163+
"Name": list_root_response["Roots"][0]["Name"],
164+
}
165+
return root_ou
166+
167+
ous = self.get_all_org_ous()
168+
for ou in ous:
169+
account_ids = [acct["Id"] for acct in self.get_accounts_for_ou(ou["Id"])]
170+
if account_id in account_ids:
171+
return ou
63172
return None
64173

65174
def get_accounts_for_ou(self, ou_id: str) -> List[AccountTypeDef]:
@@ -70,6 +179,15 @@ def get_accounts_for_ou(self, ou_id: str) -> List[AccountTypeDef]:
70179
accounts.extend(page["Accounts"])
71180
return accounts
72181

182+
def get_account_ids_in_ous(self, ou_names: List[str]) -> List[str]:
183+
ou_ids = self.get_ou_ids_from_ou_names(target_ou_names=ou_names)
184+
account_ids = []
185+
for ou_id in ou_ids:
186+
account_ids.extend(
187+
[acct["Id"] for acct in self.get_accounts_for_ou(ou_id=ou_id)]
188+
)
189+
return account_ids
190+
73191
def account_id_is_member_of_root(self, account_id: str) -> bool:
74192
root_id = self.get_root_ou_id()
75193
accounts_under_root = self.get_accounts_for_ou(ou_id=root_id)
@@ -78,9 +196,11 @@ def account_id_is_member_of_root(self, account_id: str) -> bool:
78196
def ou_contains_account(self, ou_name: str, account_id: str) -> bool:
79197
if ou_name == OrganizationsAgent.ROOT_OU:
80198
return self.account_id_is_member_of_root(account_id=account_id)
81-
current_ou = self.get_ou_id_for_account_id(account_id=account_id)
199+
current_ou = self.get_ou_from_account_id(account_id=account_id)
82200
if current_ou:
83-
current_ou_name = current_ou["OrganizationalUnit"]["Name"]
84-
if current_ou_name == ou_name:
201+
if ou_name == current_ou["Name"]:
85202
return True
86203
return False
204+
205+
def list_tags_for_resource(self, resource: str) -> List[TagTypeDef]:
206+
return self.orgs_client.list_tags_for_resource(ResourceId=resource)["Tags"]

src/aft_lambda/aft_account_provisioning_framework/aft_account_provisioning_framework_account_metadata_ssm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
from aft_common.account_provisioning_framework import (
1212
SSM_PARAMETER_PATH,
1313
ProvisionRoles,
14-
create_ssm_parameters,
1514
delete_ssm_parameters,
1615
get_ssm_parameters_names_by_path,
16+
put_ssm_parameters,
1717
)
1818
from aft_common.auth import AuthClient
1919

@@ -71,7 +71,7 @@ def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None:
7171

7272
# Update / Add SSM parameters for custom fields provided
7373
logger.info(message=f"Adding/Updating SSM params: {custom_fields}")
74-
create_ssm_parameters(target_account_session, custom_fields)
74+
put_ssm_parameters(target_account_session, custom_fields)
7575

7676
except Exception as error:
7777
notifications.send_lambda_failure_sns_message(

0 commit comments

Comments
 (0)