Delegate access across AWS accounts using IAM roles#
Keywords: AWS, IAM Role, Cross Account Access, Assume Role.
Summary#
位于 Account A (acc_A) 上的用户想要访问位于 acc_B 上的资源,但就为了这个需求而在 acc_B 上创建一个 IAM 用户, 这显然不是一个 secure 也不 scale 的做法. 如果你有 A 个人, 需要在 B 个 Account 上做 C 件需要不同权限的事, 难道你要创建 A * B * C 个 IAM User 吗? 为了解决这个问题, AWS 官方推荐使用 IAM Roles 来实现跨账号访问. 其原理简单来说就是在每个 Account 上为每一件要做的事情创建一个 IAM Role, 一共有 B * C 个 Role. 然后给每个人创建一个 IAM Entity (如果你用的 SSO 那么一般登录后对应的是一个 IAM Role, 否则一般是 IAM User), 然后允许这些 Entity assume 那些做事情的 Role, 所以总共你需要管理的 Entity 数量是 A + B * C.
本文将详细介绍如何使用 IAM Roles 来实现跨账号访问, 并且给出了代码示例和一个小工具.
Reference:
IAM tutorial: Delegate access across AWS accounts using IAM roles: https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html
How it Works#
首先我们定义我们的目标, 我们要实现 acc_A 上的用户访问 acc_B. 我们定义被访问的账号叫做 owner account, 而需要访问权限的账号叫做 grantee account.
简单来说实现这个的原理是:
在 owner account 上创建一个 IAM Role. trusted entity 里的要列出 grantee account 上需要访问权限的 principal. 这个 principal 可以是 Account Root, 也可以是特定的 IAM Group / User / Role, 也可以两种都用. 这两种方法各有优劣, 我们将在后面详细讨论.
在 grantee account 上给需要访问权限的 principal 必要的 IAM policy permission, 里面要允许它 assume 在前一步在 owner account 上创建的 IAM Role.
然后你在 grantee account 的 console 界面的右上角的 dropdown menu 里选择 Switch Role, 然后填写 owner account 的 account id 以及 role name 即可进入 owner account 的 console.
如果你要用 CLI 访问, 那么你先创建一个 boto session, 然后调用 sts.assume_role API, 它会返回一些 token, 然后你再用这些 token 创建一个新的 boto session, 这个 session 就相当于是 owner account 上的 IAM Role 了.
下面是一些 IAM Policy 的例子:
Owner account 上的 IAM Role 的 trusted entity:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::${grantee_aws_account_id}:root",
"arn:aws:iam::${grantee_aws_account_id}:role/${iam_role_on_grantee_aws_account}",
"arn:aws:iam::${grantee_aws_account_id}:user/${iam_user_on_grantee_aws_account}",
"arn:aws:iam::${grantee_aws_account_id}:group/${iam_group_on_grantee_aws_account}"
]
},
"Action": "sts:AssumeRole",
"Condition": {}
}
]
}
Grantee account 上的 Principal 所需要的 IAM Policy:
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::${owner_account_id}:role/${iam_role_on_owner_aws_account}"
}
}
这里有个 best practice, 如果你的 Grantee account 上的 principal 配置了 sts:AssumeRole
+ *
. 那么会导致你可以 assume owner account 上的 role, 这相当于默认运行了. 这种 cross account 的行文肯定应该是默认不允许的, 所以在最佳实践上你应该给除了 Admin 之外的任何人都配置 explicit deny. 这个最好是通过 User Group 来实现. 下面这个是 explicit deny 的 IAM Policy:
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Deny",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::${owner_account_id}:role/${iam_role_on_owner_aws_account}"
}
}
Best Practice#
我开发了一个自动化脚本, 能够自动的为 1 个 grantee account 和多个 owner accounts 配好 cross account access. 这个 grantee account 上的 identity 可以是整个 Account, 或是一个给人用的 IAM User, 也可以是一个给机器用的 IAM Role. 而 owner accounts 上的 Role 的权限可以是各不相同的.
1# -*- coding: utf-8 -*-
2
3"""
4This automation scripts can quickly set up a delegated cross AWS Account access
5using IAM Role.
6
7Assuming you have a grantee AWS Account and multiple owner AWS Accounts.
8The grantee AWS Account has an identity (IAM User or IAM Role) that needs to
9assume IAM Role in the owner AWS Accounts to perform some tasks on owner
10AWS Accounts.
11
12Please scroll to the bottom (below the ``if __name__ == "__main__":`` section)
13to see the example usage.
14
15Requirements:
16
17- Python3.7+
18- Dependencies::
19
20 # content of requirements.txt
21 boto3
22 cached-property>=1.5.2; python_version < '3.8'
23 boto_session_manager>=1.5.2,<2.0.0
24 aws_cloudformation>=1.5.1,<2.0.0
25"""
26
27import typing as T
28import json
29import dataclasses
30
31try:
32 from functools import cached_property
33except ImportError: # pragma: no cover
34 from cached_property import cached_property
35
36from boto_session_manager import BotoSesManager
37from aws_cloudformation import deploy_stack, remove_stack
38
39if T.TYPE_CHECKING: # pragma: no cover
40 from mypy_boto3_iam.client import IAMClient
41
42
43# ------------------------------------------------------------------------------
44# IAM Arn data models
45# ------------------------------------------------------------------------------
46@dataclasses.dataclass
47class _IamArn:
48 account: str
49
50 @property
51 def arn(self) -> str:
52 raise NotImplementedError
53
54 def attach_policy(
55 self,
56 iam_client: "IAMClient",
57 iam_policy_arn: str,
58 ):
59 raise NotImplementedError
60
61
62@dataclasses.dataclass
63class IamRootArn(_IamArn):
64 @property
65 def arn(self) -> str:
66 return f"arn:aws:iam::{self.account}:root"
67
68 @classmethod
69 def parse_arn(cls, arn: str):
70 return cls(account=arn.split(":")[4])
71
72 def attach_policy(
73 self,
74 iam_client: "IAMClient",
75 iam_policy_arn: str,
76 ):
77 pass
78
79
80@dataclasses.dataclass
81class _IamNamedArn(_IamArn):
82 name: str
83
84 def _make_arn(self, type: str) -> str:
85 return f"arn:aws:iam::{self.account}:{type}/{self.name}"
86
87 @classmethod
88 def parse_arn(cls, arn: str):
89 _, _, _, _, account, type_and_name = arn.split(":")
90 name = type_and_name.split("/", 1)[1]
91 return cls(account=account, name=name)
92
93
94@dataclasses.dataclass
95class IamGroupArn(_IamNamedArn):
96 @property
97 def arn(self) -> str:
98 return self._make_arn("group")
99
100 def attach_policy(
101 self,
102 iam_client: "IAMClient",
103 iam_policy_arn: str,
104 ):
105 iam_client.attach_group_policy(
106 GroupName=self.name,
107 PolicyArn=iam_policy_arn,
108 )
109
110
111@dataclasses.dataclass
112class IamUserArn(_IamNamedArn):
113 @property
114 def arn(self) -> str:
115 return self._make_arn("user")
116
117 def attach_policy(
118 self,
119 iam_client: "IAMClient",
120 iam_policy_arn: str,
121 ):
122 iam_client.attach_user_policy(
123 UserName=self.name,
124 PolicyArn=iam_policy_arn,
125 )
126
127
128@dataclasses.dataclass
129class IamRoleArn(_IamNamedArn):
130 @property
131 def arn(self) -> str:
132 return self._make_arn("role")
133
134 def attach_policy(
135 self,
136 iam_client: "IAMClient",
137 iam_policy_arn: str,
138 ):
139 iam_client.attach_role_policy(
140 RoleName=self.name,
141 PolicyArn=iam_policy_arn,
142 )
143
144
145@dataclasses.dataclass
146class IamPolicyArn(_IamNamedArn):
147 @property
148 def arn(self) -> str:
149 return self._make_arn("policy")
150
151
152T_GRANTEE_ARN = T.Union[IamRootArn, IamGroupArn, IamUserArn, IamRoleArn]
153T_IAM_ARN = T.Union[IamRootArn, IamGroupArn, IamUserArn, IamRoleArn, IamPolicyArn]
154
155
156# ------------------------------------------------------------------------------
157# Grantee and Owner data models
158# ------------------------------------------------------------------------------
159@dataclasses.dataclass
160class AwsContext:
161 """
162 :param bsm: the boto session manager for this AWS context.
163 """
164
165 bsm: BotoSesManager = dataclasses.field()
166
167
168def get_managed_policy_property_name(iam_arn: T_IAM_ARN) -> str:
169 """
170 Get the CloudFormation IamManagedPolicy property name for the given IAM ARN.
171 """
172 if isinstance(iam_arn, IamGroupArn):
173 return "Groups"
174 elif isinstance(iam_arn, IamRoleArn):
175 return "Roles"
176 elif isinstance(iam_arn, IamUserArn):
177 return "Users"
178 else:
179 raise TypeError
180
181
182@dataclasses.dataclass
183class Grantee(AwsContext):
184 """
185 Represents an AWS entity that needs to assume IAM Role in the owner AWS account.
186
187 :param bsm: the boto session manager for this AWS context, it is used to
188 provision necessary AWS resources for cross account access via CloudFormation.
189 :param stack_name: cloudformation stack name to set up necessary resource
190 for this grantee.
191 :param bsm: the boto session manager for this AWS context, it is used to
192 provision necessary AWS resources for cross account access via CloudFormation.
193 :param policy_name: the name of the IAM policy to attach to the grantee,
194 which allows the grantee to assume the owner's IAM role.
195 :param test_bsm: optional, the boto session manager represents the grantee
196 for cross account access testing.
197 """
198 stack_name: str = dataclasses.field()
199 iam_arn: T_IAM_ARN = dataclasses.field()
200 policy_name: str = dataclasses.field()
201 test_bsm: T.Optional[BotoSesManager] = dataclasses.field(default=None)
202 _owners: T.Dict[str, "Owner"] = dataclasses.field(default_factory=dict)
203
204 @property
205 def id(self) -> str:
206 """
207 A unique identifier for this grantee.
208 """
209 return self.iam_arn.arn
210
211 @property
212 def policy_arn(self) -> str:
213 """
214 The ARN of the IAM policy attached to this grantee.
215 """
216 return IamRoleArn(account=self.bsm.aws_account_id, name=self.policy_name).arn
217
218 @property
219 def policy_document(self) -> dict:
220 """
221 The IAM policy document attached to this grantee.
222 """
223 resource = [owner.role_arn for owner in self._owners.values()]
224 return {
225 "Version": "2012-10-17",
226 "Statement": [
227 {
228 "Effect": "Allow",
229 "Action": "sts:AssumeRole",
230 "Resource": resource,
231 },
232 ],
233 }
234
235 def is_need_deploy(self) -> bool:
236 """
237 Do we need to deploy cloudformation for this grantee?
238 """
239 if len(self._owners) == 0:
240 return False
241 if isinstance(self.iam_arn, IamRootArn):
242 return False
243 return True
244
245 @property
246 def cft(self) -> dict:
247 """
248 The cloudformation template that defines the necessary AWS resources
249 for cross account access.
250 """
251 tpl = {
252 "AWSTemplateFormatVersion": "2010-09-09",
253 "Resources": {},
254 }
255 if isinstance(self.iam_arn, IamRootArn):
256 pass
257 else:
258 managed_policy_properties = {
259 "ManagedPolicyName": self.policy_name,
260 "PolicyDocument": self.policy_document,
261 }
262 property_name = get_managed_policy_property_name(self.iam_arn)
263 managed_policy_properties[property_name] = [self.iam_arn.name]
264 tpl["Resources"]["GranteeIamPolicy"] = {
265 "Type": "AWS::IAM::ManagedPolicy",
266 "Properties": managed_policy_properties,
267 }
268 return tpl
269
270
271@dataclasses.dataclass
272class Owner(AwsContext):
273 """
274 Represents an AWS account that will allow an IAM entity from another AWS account
275 to assume an IAM role on this account for some tasks.
276
277 :param bsm: the boto session manager for this AWS context, it is used to
278 provision necessary AWS resources for cross account access via CloudFormation.
279 :param stack_name: cloudformation stack name to set up necessary resource
280 for this owner.
281 :param role_name: the name of the IAM role to be assumed by the IAM entity
282 from another AWS account.
283 :param policy_name: the name of the IAM policy to attach to the IAM role,
284 this policy defines the permissions that the IAM entity from another AWS
285 account can perform on this AWS account.
286 :param policy_document: the policy document that defines the permissions
287 that the IAM entity from another AWS account can perform on this AWS account.
288 """
289 stack_name: str = dataclasses.field()
290 role_name: str = dataclasses.field()
291 policy_name: str = dataclasses.field()
292 policy_document: dict = dataclasses.field()
293 _grantees: T.Dict[str, "Grantee"] = dataclasses.field(default_factory=dict)
294
295 @property
296 def id(self) -> str:
297 """
298 The unique identifier for this owner.
299 """
300 return self.role_arn
301
302 @property
303 def role_arn(self) -> str:
304 """
305 The ARN of the IAM role that will be assumed by the IAM entity from
306 another AWS account.
307 """
308 return IamRoleArn(account=self.bsm.aws_account_id, name=self.role_name).arn
309
310 @property
311 def policy_arn(self) -> str:
312 """
313 The ARN of the IAM policy attached to the IAM role.
314 """
315 return IamRoleArn(account=self.bsm.aws_account_id, name=self.policy_name).arn
316
317 def grant(self, grantee: "Grantee"):
318 """
319 Grant the given grantee to assume the IAM role.
320 """
321 if grantee.id not in self._grantees:
322 self._grantees[grantee.id] = grantee
323 grantee._owners[self.id] = self
324
325 def revoke(self, grantee: "Grantee"):
326 """
327 Revoke the permission of given grantee to assume the IAM role.
328 """
329 if grantee.id in self._grantees:
330 self._grantees.pop(grantee.id)
331 grantee._owners.pop(self.id)
332
333 @property
334 def trusted_entities_document(self) -> dict:
335 """
336 The IAM policy document that defines the trusted entities that who
337 can assume the IAM role.
338 """
339 arn_list = [grantee.iam_arn.arn for grantee in self._grantees.values()]
340 arn_list.sort()
341 return {
342 "Version": "2012-10-17",
343 "Statement": [
344 {
345 "Effect": "Allow",
346 "Principal": {"AWS": arn_list},
347 "Action": "sts:AssumeRole",
348 },
349 ],
350 }
351
352 def is_need_deploy(self) -> bool:
353 """
354 Do we need to deploy cloudformation for this owner?
355 """
356 if len(self._grantees) == 0:
357 return False
358 return True
359
360 @property
361 def cft(self) -> dict:
362 """
363 The cloudformation template that defines the necessary AWS resources
364 for cross account access.
365 """
366 return {
367 "AWSTemplateFormatVersion": "2010-09-09",
368 "Resources": {
369 "OwnerIamRole": {
370 "Type": "AWS::IAM::Role",
371 "Properties": {
372 "AssumeRolePolicyDocument": self.trusted_entities_document,
373 "RoleName": self.role_name,
374 "Policies": [
375 {
376 "PolicyName": f"{self.role_name}-policy",
377 "PolicyDocument": self.policy_document,
378 },
379 ],
380 },
381 }
382 },
383 }
384
385
386def ensure_no_duplicate_accounts(
387 grantee_list: T.List[Grantee],
388 owner_list: T.List[Owner],
389):
390 grantee_account_list = [grantee.bsm.aws_account_id for grantee in grantee_list]
391 owner_account_list = [owner.bsm.aws_account_id for owner in owner_list]
392 if len(set(grantee_account_list)) != len(grantee_account_list):
393 raise ValueError("Duplicate account IDs in grantee list")
394 if len(set(owner_account_list)) != len(owner_account_list):
395 raise ValueError("Duplicate account IDs in owner list")
396
397
398def deploy(
399 grantee_list: T.List[Grantee],
400 owner_list: T.List[Owner],
401 tags: T.Optional[T.Dict[str, str]] = None,
402 verbose: bool = True,
403):
404 """
405 Deploy the cross account access resources for the given grantee and owner
406
407 :param grantee_list: The list of :class:`Grantee`.
408 :param owner_list: The list of :class:`Owner`.
409 :param tags: The tags to be attached to the cloudformation stack, it will be
410 propagated to the IAM role and IAM policy.
411 :param verbose: Whether to print verbose information.
412 """
413 # ensure_no_duplicate_accounts(grantee_list, owner_list)
414 if tags is None:
415 tags = {"meta:created_by": "cross-account-iam-role-access-manager"}
416 else:
417 tags["meta:created_by"] = "cross-account-iam-role-access-manager"
418
419 for grantee in grantee_list:
420 if grantee.is_need_deploy():
421 deploy_stack(
422 bsm=grantee.bsm,
423 stack_name=grantee.stack_name,
424 template=json.dumps(grantee.cft),
425 include_iam=True,
426 include_named_iam=True,
427 tags=tags,
428 skip_plan=False,
429 skip_prompt=True,
430 wait=True,
431 timeout=60,
432 on_failure_delete=True,
433 verbose=verbose,
434 )
435
436 for owner in owner_list:
437 if owner.is_need_deploy():
438 deploy_stack(
439 bsm=owner.bsm,
440 stack_name=owner.stack_name,
441 template=json.dumps(owner.cft),
442 include_iam=True,
443 include_named_iam=True,
444 tags=tags,
445 skip_plan=False,
446 skip_prompt=True,
447 wait=True,
448 timeout=60,
449 on_failure_delete=True,
450 verbose=verbose,
451 )
452
453
454def get_account_info(bsm: BotoSesManager) -> T.Tuple[str, str, str]:
455 """
456 Get the account ID, account alias and ARN of the given boto session.
457 """
458 res = bsm.sts_client.get_caller_identity()
459 account_id = res["Account"]
460 arn = res["Arn"]
461 res = bsm.iam_client.list_account_aliases()
462 account_alias = res.get("AccountAliases", ["unknown-account-alias"])[0]
463 return account_id, account_alias, arn
464
465
466def print_account_info(bsm: BotoSesManager):
467 """
468 Display the account ID, account alias and ARN of the given boto session.
469 """
470 account_id, account_alias, arn = get_account_info(bsm)
471 print(
472 f"now we are on account {account_id} ({account_alias}), using principal {arn}"
473 )
474
475
476def validate(
477 grantee_list: T.List[Grantee],
478 call_api: T.Callable,
479 verbose: bool = True,
480):
481 """
482 Validate the cross account permission of the grantee by calling the given API
483 on the owner AWS account.
484
485 :param grantee_list: The list of :class:`Grantee`
486 :param call_api: The callable that will be called on the owner AWS account,
487 it takes a :class:`BotoSesManager` as the only argument.
488 :param verbose: Whether to print verbose information.
489 """
490 if verbose:
491 print("Verify cross account assume role ...")
492 for grantee in grantee_list:
493 if verbose:
494 account_id, account_alias, arn = get_account_info(grantee.test_bsm)
495 print(
496 f"We are on grantee account {grantee.bsm.aws_account_id} ({account_alias}), "
497 f"using principal {arn}"
498 )
499 for owner in grantee._owners.values():
500 print(f" Try to assume role {owner.role_arn} on owner account ...")
501 bsm_new = grantee.test_bsm.assume_role(role_arn=owner.role_arn)
502 call_api(bsm_new)
503
504
505def delete(
506 grantee_list: T.List[Grantee],
507 owner_list: T.List[Owner],
508 verbose: bool = True,
509):
510 """
511 Delete the cross account access resources for the given grantee and owner.
512
513 :param grantee_list: The list of :class:`Grantee`.
514 :param owner_list: The list of :class:`Owner`.
515 :param deploy_name: this name will be used as part of the cloudformation stack
516 naming convention.
517 :param verbose: Whether to print verbose information.
518 """
519 for grantee in grantee_list:
520 remove_stack(
521 bsm=grantee.bsm,
522 stack_name=grantee.stack_name,
523 skip_prompt=True,
524 wait=True,
525 timeout=60,
526 verbose=verbose,
527 )
528
529 for owner in owner_list:
530 remove_stack(
531 bsm=owner.bsm,
532 stack_name=owner.stack_name,
533 skip_prompt=True,
534 wait=True,
535 timeout=60,
536 verbose=verbose,
537 )
538
539
540if __name__ == "__main__":
541 # --------------------------------------------------------------------------
542 # Example 1. grantee are IAM account
543 # --------------------------------------------------------------------------
544 prefix = "a1b2-"
545
546 grantee_1_bsm = BotoSesManager(profile_name="bmt_app_dev_us_east_1")
547 grantee_1 = Grantee(
548 bsm=grantee_1_bsm,
549 stack_name=f"{prefix}cross-account-deployer",
550 iam_arn=IamRootArn(account=grantee_1_bsm.aws_account_id),
551 policy_name=f"{prefix}cross_account_deployer_policy",
552 test_bsm=grantee_1_bsm,
553 )
554
555 owner_1_bsm = BotoSesManager(profile_name="bmt_app_prod_us_east_1")
556 owner_1 = Owner(
557 bsm=owner_1_bsm,
558 stack_name=f"{prefix}production-account-deployer",
559 role_name=f"{prefix}production_account_deployer_role",
560 policy_name=f"{prefix}production_account_deployer_policy",
561 policy_document={
562 "Version": "2012-10-17",
563 "Statement": [
564 {
565 "Effect": "Allow",
566 "Action": "*",
567 "Resource": "*",
568 },
569 ],
570 },
571 )
572
573 owner_1.grant(grantee_1)
574
575 deploy(
576 grantee_list=[grantee_1],
577 owner_list=[owner_1],
578 )
579
580 def call_api(bsm: BotoSesManager):
581 account_id, account_alias, arn = get_account_info(bsm)
582 print(
583 f" now we are on account {account_id} ({account_alias}), using principal {arn}"
584 )
585
586 validate(
587 grantee_list=[grantee_1],
588 call_api=call_api,
589 )
590
591 # delete(
592 # grantee_list=[grantee_1],
593 # owner_list=[owner_1],
594 # )
595
596 # --------------------------------------------------------------------------
597 # Example 2. grantee are IAM User
598 # --------------------------------------------------------------------------
599 # prefix = "a1b2-"
600 #
601 # grantee_1_bsm = BotoSesManager(profile_name="bmt_app_dev_us_east_1")
602 # grantee_1 = Grantee(
603 # bsm=grantee_1_bsm,
604 # stack_name=f"{prefix}cross-account-deployer",
605 # iam_arn=IamUserArn(account=grantee_1_bsm.aws_account_id, name="sanhe"),
606 # policy_name=f"{prefix}cross_account_deployer_policy",
607 # test_bsm=grantee_1_bsm,
608 # )
609 #
610 # owner_1_bsm = BotoSesManager(profile_name="bmt_app_prod_us_east_1")
611 # owner_1 = Owner(
612 # bsm=owner_1_bsm,
613 # stack_name=f"{prefix}production-account-deployer",
614 # role_name=f"{prefix}production_account_deployer_role",
615 # policy_name=f"{prefix}production_account_deployer_policy",
616 # policy_document={
617 # "Version": "2012-10-17",
618 # "Statement": [
619 # {
620 # "Effect": "Allow",
621 # "Action": "*",
622 # "Resource": "*",
623 # },
624 # ],
625 # },
626 # )
627 #
628 # owner_1.grant(grantee_1)
629 #
630 # deploy(
631 # grantee_list=[grantee_1],
632 # owner_list=[owner_1],
633 # )
634 #
635 # def call_api(bsm: BotoSesManager):
636 # account_id, account_alias, arn = get_account_info(bsm)
637 # print(
638 # f" now we are on account {account_id} ({account_alias}), using principal {arn}"
639 # )
640 #
641 # validate(
642 # grantee_list=[grantee_1],
643 # call_api=call_api,
644 # )
645 #
646 # delete(
647 # grantee_list=[grantee_1],
648 # owner_list=[owner_1],
649 # )
650
651 # --------------------------------------------------------------------------
652 # Example 3. grantee are IAM Role
653 # --------------------------------------------------------------------------
654 # prefix = "a1b2-"
655 #
656 # grantee_1_bsm = BotoSesManager(profile_name="bmt_app_dev_us_east_1")
657 # iam_role_arn = IamRoleArn(account=grantee_1_bsm.aws_account_id, name="project-boto_session_manager")
658 # grantee_1 = Grantee(
659 # bsm=grantee_1_bsm,
660 # stack_name=f"{prefix}cross-account-deployer",
661 # iam_arn=iam_role_arn,
662 # policy_name=f"{prefix}cross_account_deployer_policy",
663 # test_bsm=grantee_1_bsm.assume_role(role_arn=iam_role_arn.arn),
664 # )
665 #
666 # owner_1_bsm = BotoSesManager(profile_name="bmt_app_prod_us_east_1")
667 # owner_1 = Owner(
668 # bsm=owner_1_bsm,
669 # stack_name=f"{prefix}production-account-deployer",
670 # role_name=f"{prefix}production_account_deployer_role",
671 # policy_name=f"{prefix}production_account_deployer_policy",
672 # policy_document={
673 # "Version": "2012-10-17",
674 # "Statement": [
675 # {
676 # "Effect": "Allow",
677 # "Action": "*",
678 # "Resource": "*",
679 # },
680 # ],
681 # },
682 # )
683 #
684 # owner_1.grant(grantee_1)
685 #
686 # deploy(
687 # grantee_list=[grantee_1],
688 # owner_list=[owner_1],
689 # )
690 #
691 # def call_api(bsm: BotoSesManager):
692 # account_id, account_alias, arn = get_account_info(bsm)
693 # print(
694 # f" now we are on account {account_id} ({account_alias}), using principal {arn}"
695 # )
696 #
697 # validate(
698 # grantee_list=[grantee_1],
699 # call_api=call_api,
700 # )
701 #
702 # delete(
703 # grantee_list=[grantee_1],
704 # owner_list=[owner_1],
705 # )