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:

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    # )