From 12203cf7141b106417b619d436d793fdc210c6d9 Mon Sep 17 00:00:00 2001 From: kukushking Date: Wed, 5 Feb 2025 13:51:17 +0000 Subject: [PATCH 01/12] add paths --- seedfarmer/__main__.py | 18 +++++++++ seedfarmer/cli_groups/_bootstrap_group.py | 24 ++++++++++++ seedfarmer/cli_groups/_project_group.py | 9 ++++- seedfarmer/commands/_bootstrap_commands.py | 24 +++++++++++- seedfarmer/commands/_deployment_commands.py | 39 +++++++++++++++++-- seedfarmer/commands/_module_commands.py | 4 +- .../commands/_project_policy_commands.py | 12 ++++-- seedfarmer/commands/_stack_commands.py | 14 ++++--- seedfarmer/mgmt/deploy_utils.py | 5 ++- .../models/manifests/_deployment_manifest.py | 4 ++ .../models/transfer/_module_deploy_object.py | 1 + seedfarmer/resources/deployment_role.template | 8 +++- seedfarmer/resources/projectpolicy.yaml | 14 +++---- seedfarmer/resources/toolchain_role.template | 4 +- seedfarmer/services/_iam.py | 12 ++++++ seedfarmer/services/session_manager.py | 7 ++++ seedfarmer/utils.py | 22 +++++++++-- test/unit-test/test_cli_arg.py | 5 +++ test/unit-test/test_commands_deployment.py | 13 +++++++ 19 files changed, 206 insertions(+), 33 deletions(-) diff --git a/seedfarmer/__main__.py b/seedfarmer/__main__.py index d750c04b..ba2689a2 100644 --- a/seedfarmer/__main__.py +++ b/seedfarmer/__main__.py @@ -64,6 +64,13 @@ def version() -> None: Use only if bootstrapped with this qualifier""", required=False, ) +@click.option( + "--role-prefix", + default="/", + help="""An IAM path prefix to use with the seedfarmer roles. + Use only if bootstrapped with this path""", + required=False, +) @click.option( "--env-file", "env_files", @@ -129,6 +136,7 @@ def apply( profile: Optional[str], region: Optional[str], qualifier: Optional[str], + role_prefix: str, env_files: List[str], debug: bool, dry_run: bool, @@ -154,6 +162,7 @@ def apply( profile=profile, region_name=region, qualifier=qualifier, + role_prefix=role_prefix, dryrun=dry_run, show_manifest=show_manifest, enable_session_timeout=enable_session_timeout, @@ -200,6 +209,13 @@ def apply( Use only if bootstrapped with this qualifier""", required=False, ) +@click.option( + "--role-prefix", + default="/", + help="""An IAM path prefix to use with the seedfarmer roles. + Use only if bootstrapped with this path""", + required=False, +) @click.option( "--env-file", "env_files", @@ -248,6 +264,7 @@ def destroy( profile: Optional[str], region: Optional[str], qualifier: Optional[str], + role_prefix: str, env_files: List[str], debug: bool, enable_session_timeout: bool, @@ -274,6 +291,7 @@ def destroy( profile=profile, region_name=region, qualifier=qualifier, + role_prefix=role_prefix, dryrun=dry_run, show_manifest=show_manifest, enable_session_timeout=enable_session_timeout, diff --git a/seedfarmer/cli_groups/_bootstrap_group.py b/seedfarmer/cli_groups/_bootstrap_group.py index ebeef8cc..6bad5956 100644 --- a/seedfarmer/cli_groups/_bootstrap_group.py +++ b/seedfarmer/cli_groups/_bootstrap_group.py @@ -101,6 +101,18 @@ def bootstrap() -> None: If used, it MUST be used on every seedfarmer command.""", required=False, ) +@click.option( + "--role-prefix", + default="/", + help="IAM Role Path prefix.", + required=False, +) +@click.option( + "--policy-prefix", + default="/", + help="IAM Policy Path prefix.", + required=False, +) @click.option( "--policy-arn", "-pa", @@ -121,6 +133,8 @@ def bootstrap_toolchain( profile: Optional[str], region: Optional[str], qualifier: Optional[str], + role_prefix: str, + policy_prefix: str, as_target: bool, synth: bool, debug: bool, @@ -140,6 +154,8 @@ def bootstrap_toolchain( policy_arns=policy_arn, profile=profile, qualifier=qualifier, + role_prefix=role_prefix, + policy_prefix=policy_prefix, region_name=region, synthesize=synth, as_target=as_target, @@ -197,6 +213,12 @@ def bootstrap_toolchain( If used on the toolchain account, it should be used here!""", required=False, ) +@click.option( + "--role-prefix", + default="/", + help="IAM Role Path prefix.", + required=False, +) @click.option( "--policy-arn", "-pa", @@ -215,6 +237,7 @@ def bootstrap_target( profile: Optional[str], region: Optional[str], qualifier: Optional[str], + role_prefix: str, synth: bool, debug: bool, ) -> None: @@ -229,6 +252,7 @@ def bootstrap_target( profile=profile, region_name=region, qualifier=qualifier, + role_prefix=role_prefix, permissions_boundary_arn=permissions_boundary, policy_arns=policy_arn, synthesize=synth, diff --git a/seedfarmer/cli_groups/_project_group.py b/seedfarmer/cli_groups/_project_group.py index ddbe52ea..be38d5e8 100644 --- a/seedfarmer/cli_groups/_project_group.py +++ b/seedfarmer/cli_groups/_project_group.py @@ -37,11 +37,18 @@ def projectpolicy() -> None: name="synth", help="Synth a Project Policy from seed-farmer.", ) +@click.option( + "--policy-prefix", + default="/", + help="An IAM path prefix to use with the policy.", + required=False, +) @click.option("--debug/--no-debug", default=False, help="Enable detail logging", show_default=True) def policy_synth( + policy_prefix: str, debug: bool, ) -> None: if debug: enable_debug(format=DEBUG_LOGGING_FORMAT) - get_default_project_policy() + get_default_project_policy(policy_prefix=policy_prefix) diff --git a/seedfarmer/commands/_bootstrap_commands.py b/seedfarmer/commands/_bootstrap_commands.py index 5b03ab5b..a033136d 100644 --- a/seedfarmer/commands/_bootstrap_commands.py +++ b/seedfarmer/commands/_bootstrap_commands.py @@ -39,6 +39,7 @@ def get_toolchain_template( principal_arn: List[str], role_name: str, permissions_boundary_arn: Optional[str] = None, + role_prefix: str = "/", ) -> Dict[Any, Any]: with open((os.path.join(CLI_ROOT, "resources/toolchain_role.template")), "r") as f: role = yaml.safe_load(f) @@ -49,7 +50,14 @@ def get_toolchain_template( if permissions_boundary_arn: role["Resources"]["ToolchainRole"]["Properties"]["PermissionsBoundary"] = permissions_boundary_arn template = Template(json.dumps(role)) - t = template.render({"project_name": project_name, "role_name": role_name, "seedfarmer_version": __version__}) + t = template.render( + { + "project_name": project_name, + "role_name": role_name, + "role_prefix": role_prefix, + "seedfarmer_version": __version__, + } + ) return dict(json.loads(t)) @@ -59,6 +67,8 @@ def get_deployment_template( role_name: str, policy_arns: Optional[List[str]], permissions_boundary_arn: Optional[str] = None, + role_prefix: str = "/", + policy_prefix: str = "/", ) -> Dict[Any, Any]: with open((os.path.join(CLI_ROOT, "resources/deployment_role.template")), "r") as f: role = yaml.safe_load(f) @@ -72,6 +82,8 @@ def get_deployment_template( "toolchain_role_arn": toolchain_role_arn, "project_name": project_name, "role_name": role_name, + "role_prefix": role_prefix, + "policy_prefix": policy_prefix, "seedfarmer_version": __version__, } ) @@ -89,6 +101,8 @@ def bootstrap_toolchain_account( permissions_boundary_arn: Optional[str] = None, policy_arns: Optional[List[str]] = None, qualifier: Optional[str] = None, + role_prefix: str = "/", + policy_prefix: str = "/", profile: Optional[str] = None, region_name: Optional[str] = None, synthesize: bool = False, @@ -107,6 +121,7 @@ def bootstrap_toolchain_account( role_name=role_stack_name, principal_arn=principal_arns, permissions_boundary_arn=permissions_boundary_arn, + role_prefix=role_prefix, ) _logger.debug((json.dumps(template, indent=4))) if not synthesize: @@ -124,6 +139,8 @@ def bootstrap_toolchain_account( toolchain_account_id=session_account_id, project_name=project_name, qualifier=cast(str, qualifier), + role_prefix=role_prefix, + policy_prefix=policy_prefix, permissions_boundary_arn=permissions_boundary_arn, profile=profile, region_name=region_name, @@ -152,6 +169,8 @@ def bootstrap_target_account( project_name: str, permissions_boundary_arn: Optional[str] = None, qualifier: Optional[str] = None, + role_prefix: str = "/", + policy_prefix: str = "/", profile: Optional[str] = None, region_name: Optional[str] = None, session: Optional[Session] = None, @@ -171,6 +190,7 @@ def bootstrap_target_account( toolchain_account_id=toolchain_account_id, project_name=project_name, qualifier=cast(str, qualifier), + role_prefix=role_prefix, ) template = get_deployment_template( @@ -179,6 +199,8 @@ def bootstrap_target_account( role_name=role_stack_name, policy_arns=policy_arns if policy_arns else None, permissions_boundary_arn=permissions_boundary_arn, + role_prefix=role_prefix, + policy_prefix=policy_prefix, ) _logger.debug((json.dumps(template, indent=4))) if not synthesize: diff --git a/seedfarmer/commands/_deployment_commands.py b/seedfarmer/commands/_deployment_commands.py index 74fabd67..00c96c57 100644 --- a/seedfarmer/commands/_deployment_commands.py +++ b/seedfarmer/commands/_deployment_commands.py @@ -53,7 +53,7 @@ print_modules_build_info, ) from seedfarmer.services import get_sts_identity_info -from seedfarmer.services._iam import get_role +from seedfarmer.services._iam import get_role, get_role_arn from seedfarmer.services.session_manager import SessionManager from seedfarmer.utils import get_generic_module_deployment_role_name @@ -119,6 +119,10 @@ def create_generic_module_deployment_role( deployment_name=cast(str, deployment_manifest.name), region=region, ) + target_account_mapping = deployment_manifest.get_target_account_mapping(account_id=account_id) + role_prefix = ( + target_account_mapping.role_prefix if target_account_mapping and target_account_mapping.role_prefix else "/" + ) create_module_deployment_role( role_name=role_name, deployment_name=cast(str, deployment_manifest.name), @@ -132,6 +136,7 @@ def create_generic_module_deployment_role( region=region, ), session=session, + role_prefix=role_prefix, ) return role_name @@ -187,6 +192,10 @@ def _execute_deploy( deployment_name=cast(str, mdo.deployment_manifest.name), region=region, ) + target_account_mapping = mdo.deployment_manifest.get_target_account_mapping(account_id=account_id) + role_prefix = ( + target_account_mapping.role_prefix if target_account_mapping and target_account_mapping.role_prefix else "/" + ) if module_stack_path: _, module_role_name = commands.deploy_module_stack( @@ -199,12 +208,15 @@ def _execute_deploy( parameters=mdo.parameters, docker_credentials_secret=mdo.docker_credentials_secret, permissions_boundary_arn=mdo.permissions_boundary_arn, + role_prefix=role_prefix, ) - mdo.module_role_name = module_role_name - # Get the current module's SSM if it was already loaded... session = SessionManager().get_or_create().get_deployment_session(account_id=account_id, region_name=region) + + mdo.module_role_name = module_role_name + mdo.module_role_arn = get_role_arn(role_name=module_role_name, session=session) + mdo.module_metadata = json.dumps( get_module_metadata(cast(str, mdo.deployment_manifest.name), mdo.group_name, mdo.module_name, session=session) ) @@ -239,8 +251,14 @@ def _execute_destroy(mdo: ModuleDeployObject) -> Optional[ModuleDeploymentRespon target_account_id = cast(str, module_manifest.get_target_account_id()) target_region = cast(str, module_manifest.target_region) + target_account_mapping = mdo.deployment_manifest.get_target_account_mapping(account_id=target_account_id) + role_prefix = ( + target_account_mapping.role_prefix if target_account_mapping and target_account_mapping.role_prefix else "/" + ) session = ( - SessionManager().get_or_create().get_deployment_session(account_id=target_account_id, region_name=target_region) + SessionManager() + .get_or_create(role_prefix=role_prefix) + .get_deployment_session(account_id=target_account_id, region_name=target_region) ) module_metadata = get_module_metadata( cast(str, mdo.deployment_manifest.name), mdo.group_name, mdo.module_name, session=session @@ -288,6 +306,9 @@ def _execute_destroy(mdo: ModuleDeployObject) -> Optional[ModuleDeploymentRespon module_role_name=mdo.module_role_name, ) + mdo.module_role_name = module_role_name + mdo.module_role_arn = get_role_arn(role_name=module_role_name, session=session) + resp = commands.destroy_module(mdo) if resp.status == StatusType.SUCCESS.value and module_stack_exists: @@ -415,7 +436,10 @@ def _prime_accounts(args: Dict[str, Any]) -> List[Any]: "region": target_account_region["region"], "update_seedkit": update_seedkit, "update_project_policy": update_project_policy, + "role_prefix": target_account_region["role_prefix"], + "policy_prefix": target_account_region["policy_prefix"], } + if target_account_region["network"] is not None: network = commands.load_network_values( cast(NetworkMapping, target_account_region["network"]), @@ -735,6 +759,7 @@ def apply( profile: Optional[str] = None, region_name: Optional[str] = None, qualifier: Optional[str] = None, + role_prefix: str = "/", dryrun: bool = False, show_manifest: bool = False, enable_session_timeout: bool = False, @@ -760,6 +785,9 @@ def apply( qualifier : str, optional Any qualifier on the name of toolchain role Defaults to None + path : str, optional + IAM path on the ARN of the toolchain and deployment role + Defaults to None dryrun : bool, optional This flag indicates that the deployment manifest should be consumed and a DeploymentManifest object be created (for both apply and destroy) but DOES NOT @@ -799,6 +827,7 @@ def apply( project_name=config.PROJECT, profile=profile, qualifier=qualifier, + role_prefix=role_prefix, toolchain_region=deployment_manifest.toolchain_region, region_name=region_name, enable_reaper=enable_session_timeout, @@ -879,6 +908,7 @@ def destroy( profile: Optional[str] = None, region_name: Optional[str] = None, qualifier: Optional[str] = None, + role_prefix: str = "/", dryrun: bool = False, show_manifest: bool = False, remove_seedkit: bool = False, @@ -934,6 +964,7 @@ def destroy( profile=profile, region_name=region_name, qualifier=qualifier, + role_prefix=role_prefix, enable_reaper=enable_session_timeout, reaper_interval=session_timeout_interval, ) diff --git a/seedfarmer/commands/_module_commands.py b/seedfarmer/commands/_module_commands.py index c3f46d45..c263334a 100644 --- a/seedfarmer/commands/_module_commands.py +++ b/seedfarmer/commands/_module_commands.py @@ -206,7 +206,7 @@ def deploy_module(mdo: ModuleDeployObject) -> ModuleDeploymentResponse: + store_sf_bundle, extra_env_vars=env_vars, codebuild_compute_type=module_manifest.deploy_spec.build_type, - codebuild_role_name=mdo.module_role_name, + codebuild_role_name=mdo.module_role_arn, codebuild_image=active_codebuild_image, npm_mirror=npm_mirror, pypi_mirror=pypi_mirror, @@ -311,7 +311,7 @@ def destroy_module(mdo: ModuleDeployObject) -> ModuleDeploymentResponse: extra_post_build_commands=["cd module/"] + _phases.post_build.commands + remove_ssm + remove_sf_bundle, extra_env_vars=env_vars, codebuild_compute_type=module_manifest.deploy_spec.build_type, - codebuild_role_name=mdo.module_role_name, + codebuild_role_name=mdo.module_role_arn, codebuild_image=active_codebuild_image, npm_mirror=npm_mirror, pypi_mirror=pypi_mirror, diff --git a/seedfarmer/commands/_project_policy_commands.py b/seedfarmer/commands/_project_policy_commands.py index 4f74fb8e..c4903475 100644 --- a/seedfarmer/commands/_project_policy_commands.py +++ b/seedfarmer/commands/_project_policy_commands.py @@ -13,12 +13,18 @@ # limitations under the License. import os -import shutil import sys +import yaml + from seedfarmer import CLI_ROOT, DEFAULT_PROJECT_POLICY_PATH -def get_default_project_policy() -> None: +def get_default_project_policy(policy_prefix: str = "/") -> None: with open(os.path.join(CLI_ROOT, DEFAULT_PROJECT_POLICY_PATH), "rb") as f: - shutil.copyfileobj(f, sys.stdout.buffer) + policy = yaml.safe_load(f) + + if policy_prefix: + policy["Resources"]["ProjectPolicy"]["Properties"]["Path"] = policy_prefix + + sys.stdout.write(yaml.dump(policy)) diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py index 51fce1bf..33e55d2a 100644 --- a/seedfarmer/commands/_stack_commands.py +++ b/seedfarmer/commands/_stack_commands.py @@ -130,6 +130,7 @@ def create_module_deployment_role( docker_credentials_secret: Optional[str] = None, permissions_boundary_arn: Optional[str] = None, session: Optional[boto3.Session] = None, + role_prefix: str = "/", ) -> None: iam.create_check_iam_role( project_name=config.PROJECT, @@ -145,6 +146,7 @@ def create_module_deployment_role( role_name=role_name, permissions_boundary_arn=permissions_boundary_arn, session=session, + role_prefix=role_prefix, ) policies = [] @@ -219,9 +221,7 @@ def deploy_bucket_storage_stack( account_id: str The Account Id where the module is deployed region: str - The region - - + The region where the module is deployed Returns ------- @@ -272,7 +272,7 @@ def deploy_managed_policy_stack( account_id: str The Account Id where the module is deployed region: str - The region + The region where the module is deployed update_project_policy: bool Force update the project policy if already deployed """ @@ -307,7 +307,7 @@ def destroy_bucket_storage_stack( ) -> None: """ destroy_bucket_storage_stack - This function destroys the buckeet stack for SeedFarmer + This function destroys the bucket stack for SeedFarmer Parameters ---------- @@ -352,7 +352,7 @@ def destroy_managed_policy_stack(account_id: str, region: str) -> None: account_id: str The Account Id where the module is deployed region: str - The region wher + The region where the module is deployed """ # Determine if managed policy stack already deployed session = SessionManager().get_or_create().get_deployment_session(account_id=account_id, region_name=region) @@ -440,6 +440,7 @@ def deploy_module_stack( parameters: List[ModuleParameter], docker_credentials_secret: Optional[str] = None, permissions_boundary_arn: Optional[str] = None, + role_prefix: str = "/", ) -> Tuple[str, str]: """ deploy_module_stack @@ -483,6 +484,7 @@ def deploy_module_stack( docker_credentials_secret=docker_credentials_secret, permissions_boundary_arn=permissions_boundary_arn, session=session, + role_prefix=role_prefix, ) _logger.debug("module_role_name %s", module_role_name) diff --git a/seedfarmer/mgmt/deploy_utils.py b/seedfarmer/mgmt/deploy_utils.py index 38588b66..4622f02d 100644 --- a/seedfarmer/mgmt/deploy_utils.py +++ b/seedfarmer/mgmt/deploy_utils.py @@ -113,7 +113,10 @@ def _get_module_info(args: Dict[str, Any]) -> None: ) params = [ - {"account_id": target_account_region["account_id"], "region": target_account_region["region"]} + { + "account_id": target_account_region["account_id"], + "region": target_account_region["region"], + } for target_account_region in deployment_manifest.target_accounts_regions ] _ = list(workers.map(_get_module_info, params)) diff --git a/seedfarmer/models/manifests/_deployment_manifest.py b/seedfarmer/models/manifests/_deployment_manifest.py index 2a50d44a..760267b9 100644 --- a/seedfarmer/models/manifests/_deployment_manifest.py +++ b/seedfarmer/models/manifests/_deployment_manifest.py @@ -83,6 +83,8 @@ class TargetAccountMapping(CamelModel): pypi_mirror_secret: Optional[str] = None _default_region: Optional[RegionMapping] = PrivateAttr(default=None) _region_index: Dict[str, RegionMapping] = PrivateAttr(default_factory=dict) + role_prefix: Optional[str] = None + policy_prefix: Optional[str] = None def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -238,6 +240,8 @@ def target_accounts_regions(self) -> List[Dict[str, str]]: "network": region.network, # type: ignore "parameters_regional": region.parameters_regional, # type: ignore "codebuild_image": cast(str, region.codebuild_image), + "role_prefix": target_account.role_prefix, # type: ignore + "policy_prefix": target_account.policy_prefix, # type: ignore } ) return self._accounts_regions diff --git a/seedfarmer/models/transfer/_module_deploy_object.py b/seedfarmer/models/transfer/_module_deploy_object.py index a77d0f50..dff99d46 100644 --- a/seedfarmer/models/transfer/_module_deploy_object.py +++ b/seedfarmer/models/transfer/_module_deploy_object.py @@ -14,6 +14,7 @@ class ModuleDeployObject(CamelModel): docker_credentials_secret: Optional[str] = None permissions_boundary_arn: Optional[str] = None module_role_name: Optional[str] = None + module_role_arn: Optional[str] = None codebuild_image: Optional[str] = None npm_mirror: Optional[str] = None npm_mirror_secret: Optional[str] = None diff --git a/seedfarmer/resources/deployment_role.template b/seedfarmer/resources/deployment_role.template index e9aecfab..160bd553 100644 --- a/seedfarmer/resources/deployment_role.template +++ b/seedfarmer/resources/deployment_role.template @@ -14,7 +14,7 @@ Resources: Effect: Allow Principal: AWS: "{{ toolchain_role_arn }}" - Path: / + Path: "{{ role_prefix }}" Policies: - PolicyName: InlineToolchain PolicyDocument: @@ -64,6 +64,10 @@ Resources: - iam:List* Effect: Allow Resource: + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role{{ role_prefix }}{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role{{ role_prefix }}codeseeder-{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy{{ policy_prefix }}{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy{{ policy_prefix }}codeseeder-{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/codeseeder-{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/{{ project_name }}-*" @@ -116,7 +120,7 @@ Resources: - sts:GetSessionToken Effect: Allow Resource: - - Fn::Sub: "arn:${AWS::Partition}:iam::*:role/{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::*:role{{ role_prefix }}{{ project_name }}-*" Sid: DeploymentSTS - Action: - ssm:Put* diff --git a/seedfarmer/resources/projectpolicy.yaml b/seedfarmer/resources/projectpolicy.yaml index 8c74e861..3aad0271 100644 --- a/seedfarmer/resources/projectpolicy.yaml +++ b/seedfarmer/resources/projectpolicy.yaml @@ -44,7 +44,7 @@ Resources: - ssm:DeleteParameter - ssm:DeleteParameters Resource: - - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}*" + - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}*" - Effect: Allow Action: - logs:CreateLogStream @@ -54,23 +54,23 @@ Resources: - logs:GetLogGroupFields - logs:GetQueryResults Resource: - - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/codeseeder-${ProjectName}*" + - Fn::Sub: "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/codeseeder-${ProjectName}*" - Effect: Allow Action: - ssm:GetParameter Resource: - - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/cdk-bootstrap*" + - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/cdk-bootstrap*" - Effect: Allow Action: - iam:UpdateAssumeRolePolicy Resource: - - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ProjectName}*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ProjectName}*" - Effect: Allow Action: - "iam:PassRole" - "sts:AssumeRole" Resource: - - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk*" - Action: - s3:Delete* - s3:Put* @@ -85,5 +85,5 @@ Resources: Outputs: ProjectPolicyARN: - Value: !Ref 'ProjectPolicy' - + Value: + Ref: ProjectPolicy diff --git a/seedfarmer/resources/toolchain_role.template b/seedfarmer/resources/toolchain_role.template index 2ea880e2..9439dbbc 100644 --- a/seedfarmer/resources/toolchain_role.template +++ b/seedfarmer/resources/toolchain_role.template @@ -14,7 +14,7 @@ Resources: Effect: Allow Principal: AWS: [] - Path: / + Path: "{{ role_prefix }}" Policies: - PolicyName: InlineToolchain PolicyDocument: @@ -25,7 +25,7 @@ Resources: - sts:GetSessionToken Effect: Allow Resource: - Fn::Sub: "arn:${AWS::Partition}:iam::*:role/seedfarmer-{{ project_name }}*" + - Fn::Sub: "arn:${AWS::Partition}:iam::*:role{{ role_prefix }}seedfarmer-{{ project_name }}*" Sid: ToolChainSTS - Action: - ssm:Put* diff --git a/seedfarmer/services/_iam.py b/seedfarmer/services/_iam.py index d24963f4..bfc6f094 100644 --- a/seedfarmer/services/_iam.py +++ b/seedfarmer/services/_iam.py @@ -18,6 +18,7 @@ from boto3 import Session +import seedfarmer.errors from seedfarmer.services._service_utils import boto3_client, boto3_resource, get_region _logger: logging.Logger = logging.getLogger(__name__) @@ -33,6 +34,15 @@ def get_role(role_name: str, session: Optional[Session] = None) -> Optional[Dict return None +def get_role_arn(role_name: str, session: Optional[Session] = None) -> str: + role = get_role(role_name=role_name, session=session) + + if not role: + raise seedfarmer.errors.InvalidConfigurationError(f"Role {role_name} does not exist") + + return cast(str, role["Role"]["Arn"]) + + def create_check_iam_role( project_name: str, deployment_name: str, @@ -42,6 +52,7 @@ def create_check_iam_role( group_name: Optional[str] = None, module_name: Optional[str] = None, session: Optional[Session] = None, + role_prefix: str = "/", ) -> None: _logger.debug("Creating IAM Role with name: %s ", role_name) iam_client = boto3_client("iam", session=session) @@ -50,6 +61,7 @@ def create_check_iam_role( except iam_client.exceptions.NoSuchEntityException: args: Dict[str, Any] = { "RoleName": role_name, + "Path": role_prefix, "AssumeRolePolicyDocument": json.dumps(trust_policy), "Description": f"deployment-role for {role_name}", "Tags": [ diff --git a/seedfarmer/services/session_manager.py b/seedfarmer/services/session_manager.py index f247fc43..b856925c 100644 --- a/seedfarmer/services/session_manager.py +++ b/seedfarmer/services/session_manager.py @@ -49,6 +49,7 @@ def get_or_create( region_name: Optional[str] = None, toolchain_region: Optional[str] = None, qualifier: Optional[str] = None, + role_prefix: str = "/", profile: Optional[str] = None, enable_reaper: bool = False, **kwargs: Optional[Any], @@ -86,6 +87,7 @@ def get_or_create( profile: Optional[str] = None, toolchain_region: Optional[str] = None, qualifier: Optional[str] = None, + role_prefix: str = "/", reaper_interval: Optional[int] = None, enable_reaper: bool = False, **kwargs: Optional[Any], @@ -100,6 +102,7 @@ def get_or_create( self.config["profile"] = profile self.config["toolchain_region"] = toolchain_region self.config["qualifier"] = qualifier if qualifier else None + self.config["role_prefix"] = role_prefix self.config = {**self.config, **kwargs} self.toolchain_role_name = get_toolchain_role_name(project_name, cast(str, qualifier)) @@ -140,6 +143,7 @@ def get_deployment_session(self, account_id: str, region_name: str) -> Session: session_key = f"{account_id}-{region_name}" project_name = self.config["project_name"] qualifier = self.config.get("qualifier") if self.config.get("qualifier") else None + role_prefix = self.config.get("role_prefix", "/") toolchain_region = self.config.get("toolchain_region") if not self.created: raise seedfarmer.errors.InvalidConfigurationError("The SessionManager object was never properly created...") @@ -161,6 +165,7 @@ def get_deployment_session(self, account_id: str, region_name: str) -> Session: deployment_account_id=account_id, project_name=project_name, qualifier=cast(str, qualifier), + role_prefix=role_prefix, ) _logger.debug( f"""The assumed toolchain role {toolchain_role["AssumedRoleUser"]["Arn"]} will @@ -207,6 +212,7 @@ def _get_toolchain(self) -> Tuple[Session, "AssumeRoleResponseTypeDef"]: profile_name = self.config.get("profile") project_name = self.config.get("project_name") qualifier = self.config.get("qualifier") if self.config.get("qualifier") else None + role_prefix = self.config.get("role_prefix", "/") toolchain_region = self.config.get("toolchain_region") _logger.debug( f"""Creating a local session with the following info passed in: @@ -226,6 +232,7 @@ def _get_toolchain(self) -> Tuple[Session, "AssumeRoleResponseTypeDef"]: toolchain_account_id=user_account_id, project_name=cast(str, project_name), qualifier=cast(str, qualifier), + role_prefix=role_prefix, ) _logger.debug( f"""The active user session will assume the toolchain role diff --git a/seedfarmer/utils.py b/seedfarmer/utils.py index 078bbb77..e1461690 100644 --- a/seedfarmer/utils.py +++ b/seedfarmer/utils.py @@ -144,9 +144,16 @@ def get_toolchain_role_name(project_name: str, qualifier: Optional[str] = None) def get_toolchain_role_arn( - partition: str, toolchain_account_id: str, project_name: str, qualifier: Optional[str] = None + partition: str, + toolchain_account_id: str, + project_name: str, + qualifier: Optional[str] = None, + role_prefix: str = "/", ) -> str: - return f"arn:{partition}:iam::{toolchain_account_id}:role/{get_toolchain_role_name(project_name, qualifier)}" + return ( + f"arn:{partition}:iam::{toolchain_account_id}:role{role_prefix}" + f"{get_toolchain_role_name(project_name, qualifier)}" + ) def get_deployment_role_name(project_name: str, qualifier: Optional[str] = None) -> str: @@ -155,9 +162,16 @@ def get_deployment_role_name(project_name: str, qualifier: Optional[str] = None) def get_deployment_role_arn( - partition: str, deployment_account_id: str, project_name: str, qualifier: Optional[str] = None + partition: str, + deployment_account_id: str, + project_name: str, + qualifier: Optional[str] = None, + role_prefix: str = "/", ) -> str: - return f"arn:{partition}:iam::{deployment_account_id}:role/{get_deployment_role_name(project_name, qualifier)}" + return ( + f"arn:{partition}:iam::{deployment_account_id}:role{role_prefix}" + f"{get_deployment_role_name(project_name, qualifier)}" + ) def get_generic_module_deployment_role_name( diff --git a/test/unit-test/test_cli_arg.py b/test/unit-test/test_cli_arg.py index db738bf2..61f289af 100644 --- a/test/unit-test/test_cli_arg.py +++ b/test/unit-test/test_cli_arg.py @@ -1471,6 +1471,11 @@ def test_get_projectpolicy_debug(): _test_command(sub_command=projectpolicy, options=["synth", "--debug"], exit_code=0) +@pytest.mark.projectpolicy +def test_get_projectpolicy_prefix(): + _test_command(sub_command=projectpolicy, options=["synth", "--policy-prefix", "/test/"], exit_code=0) + + @pytest.mark.metadata def test_metadata_param_value(mocker): mocker.patch( diff --git a/test/unit-test/test_commands_deployment.py b/test/unit-test/test_commands_deployment.py index a66cad86..b101559a 100644 --- a/test/unit-test/test_commands_deployment.py +++ b/test/unit-test/test_commands_deployment.py @@ -173,6 +173,10 @@ def test_execute_deploy_invalid_spec(session_manager, mocker): return_value=("stack_name", "role_name"), ) mocker.patch("seedfarmer.commands._deployment_commands.get_module_metadata", return_value=None) + mocker.patch( + "seedfarmer.commands._deployment_commands.get_role_arn", + return_value="role_arn", + ) mocker.patch("seedfarmer.commands._deployment_commands.du.prepare_ssm_for_deploy", return_value=None) dep = DeploymentManifest(**mock_deployment_manifest_huge.deployment_manifest) dep.validate_and_set_module_defaults() @@ -192,6 +196,10 @@ def test_execute_deploy(session_manager, mocker): return_value=("stack_name", "role_name"), ) mocker.patch("seedfarmer.commands._deployment_commands.get_module_metadata", return_value=None) + mocker.patch( + "seedfarmer.commands._deployment_commands.get_role_arn", + return_value="role_arn", + ) mocker.patch("seedfarmer.commands._deployment_commands.du.prepare_ssm_for_deploy", return_value=None) mocker.patch("seedfarmer.commands._deployment_commands.commands.deploy_module", return_value=None) dep = DeploymentManifest(**mock_deployment_manifest_huge.deployment_manifest) @@ -240,6 +248,10 @@ def test_execute_destroy(session_manager, mocker): "seedfarmer.commands._deployment_commands.commands.get_module_stack_info", return_value=("stack_name", "role_name", True), ) + mocker.patch( + "seedfarmer.commands._deployment_commands.get_role_arn", + return_value="role_arn", + ) mocker.patch("seedfarmer.commands._deployment_commands.commands.destroy_module", return_value=mod_resp) mocker.patch("seedfarmer.commands._deployment_commands.commands.destroy_module_stack", return_value=None) mocker.patch("seedfarmer.commands._deployment_commands.commands.force_manage_policy_attach", return_value=None) @@ -313,6 +325,7 @@ def test_create_module_deployment_role(session_manager, mocker): permissions_boundary_arn="arn:aws:iam::123456789012:policy/boundary", docker_credentials_secret=None, session=ANY, + role_prefix="/", ) From 7f0662e05730a1dd6406b53a085145b57996937a Mon Sep 17 00:00:00 2001 From: kukushking Date: Wed, 5 Feb 2025 17:09:58 +0000 Subject: [PATCH 02/12] add prefixes to seedkit --- seedfarmer/commands/_deployment_commands.py | 13 +++++++---- seedfarmer/commands/_stack_commands.py | 24 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/seedfarmer/commands/_deployment_commands.py b/seedfarmer/commands/_deployment_commands.py index 00c96c57..27a37cd7 100644 --- a/seedfarmer/commands/_deployment_commands.py +++ b/seedfarmer/commands/_deployment_commands.py @@ -493,7 +493,12 @@ def _teardown_accounts(args: Dict[str, Any]) -> None: commands.destroy_seedkit(**args) params = [ - {"account_id": target_account_region["account_id"], "region": target_account_region["region"]} + { + "account_id": target_account_region["account_id"], + "region": target_account_region["region"], + "role_prefix": target_account_region["role_prefix"], + "policy_prefix": target_account_region["policy_prefix"], + } for target_account_region in deployment_manifest.target_accounts_regions ] _ = list(workers.map(_teardown_accounts, params)) @@ -785,9 +790,9 @@ def apply( qualifier : str, optional Any qualifier on the name of toolchain role Defaults to None - path : str, optional - IAM path on the ARN of the toolchain and deployment role - Defaults to None + role_prefix : str, optional + IAM path prefix on the ARN of the toolchain and deployment roles + Defaults to '/' dryrun : bool, optional This flag indicates that the deployment manifest should be consumed and a DeploymentManifest object be created (for both apply and destroy) but DOES NOT diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py index 33e55d2a..83722a68 100644 --- a/seedfarmer/commands/_stack_commands.py +++ b/seedfarmer/commands/_stack_commands.py @@ -18,9 +18,11 @@ import time from typing import Any, Dict, List, Optional, Tuple, cast +import aws_codeseeder as cs import boto3 from aws_codeseeder import EnvVar, EnvVarType, codeseeder, commands, services from cfn_tools import load_yaml +from packaging import version import seedfarmer.errors import seedfarmer.services._iam as iam @@ -342,7 +344,7 @@ def destroy_bucket_storage_stack( return -def destroy_managed_policy_stack(account_id: str, region: str) -> None: +def destroy_managed_policy_stack(account_id: str, region: str, **kwargs: Any) -> None: """ destroy_managed_policy_stack This function destroys the deployment-specific policy. @@ -471,7 +473,11 @@ def deploy_module_stack( _logger.debug(module_stack_path) - session = SessionManager().get_or_create().get_deployment_session(account_id=account_id, region_name=region) + session = ( + SessionManager() + .get_or_create(role_prefix=role_prefix) + .get_deployment_session(account_id=account_id, region_name=region) + ) module_stack_name, module_role_name = get_module_stack_names( deployment_name, group_name, module_name, session=session ) @@ -583,6 +589,8 @@ def deploy_seedkit( private_subnet_ids: Optional[List[str]] = None, security_group_ids: Optional[List[str]] = None, update_seedkit: Optional[bool] = False, + role_prefix: str = "/", + policy_prefix: str = "/", **kwargs: Any, ) -> Dict[str, Any]: """ @@ -609,6 +617,14 @@ def deploy_seedkit( _logger.debug("SeedKit exists and not updating for Account/Region: %s/%s", account_id, region) else: _logger.debug("Initializing / Updating SeedKit for Account/Region: %s/%s", account_id, region) + + kwargs = {} + if version.parse(cs.__version__) >= version.parse("1.3.0"): + kwargs = { + "role_prefix": role_prefix, + "policy_prefix": policy_prefix, + } + commands.deploy_seedkit( seedkit_name=config.PROJECT, deploy_codeartifact=deploy_codeartifact, @@ -616,6 +632,7 @@ def deploy_seedkit( vpc_id=vpc_id, subnet_ids=private_subnet_ids, security_group_ids=security_group_ids, + **kwargs, ) # Go get the outputs and return them _, _, stack_outputs = commands.seedkit_deployed(seedkit_name=config.PROJECT, session=session) @@ -632,7 +649,8 @@ def destroy_seedkit(account_id: str, region: str) -> None: account_id: str The Account Id where the module is deployed region: str - The region wher""" + The region where the module is deployed + """ session = SessionManager().get_or_create().get_deployment_session(account_id=account_id, region_name=region) _logger.debug("Destroying SeedKit for Account/Region: %s/%s", account_id, region) commands.destroy_seedkit(seedkit_name=config.PROJECT, session=session) From e79fee6fd90bacd9492e02c1fb005f4bad634059 Mon Sep 17 00:00:00 2001 From: kukushking Date: Wed, 5 Feb 2025 20:58:50 +0000 Subject: [PATCH 03/12] add prefixes to module index --- seedfarmer/mgmt/deploy_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seedfarmer/mgmt/deploy_utils.py b/seedfarmer/mgmt/deploy_utils.py index 4622f02d..26ccf466 100644 --- a/seedfarmer/mgmt/deploy_utils.py +++ b/seedfarmer/mgmt/deploy_utils.py @@ -96,8 +96,10 @@ def populate_module_info_index(deployment_manifest: DeploymentManifest) -> Modul with concurrent.futures.ThreadPoolExecutor(max_workers=len(deployment_manifest.target_accounts_regions)) as workers: def _get_module_info(args: Dict[str, Any]) -> None: + role_prefix = args.pop("role_prefix", "/") session = ( SessionManager() + .get_or_create(role_prefix=role_prefix) .get_or_create() .get_deployment_session(account_id=args["account_id"], region_name=args["region"]) ) @@ -116,6 +118,7 @@ def _get_module_info(args: Dict[str, Any]) -> None: { "account_id": target_account_region["account_id"], "region": target_account_region["region"], + "role_prefix": target_account_region.get("role_prefix", "/"), } for target_account_region in deployment_manifest.target_accounts_regions ] From 882a81e00910a781dfe09f587ddf6b745c191421 Mon Sep 17 00:00:00 2001 From: kukushking Date: Wed, 5 Feb 2025 21:01:40 +0000 Subject: [PATCH 04/12] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f04adf..2242fc5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a Ch ## Unreleased ### New +- adding support for IAM paths ### Changes From ea48146a17afa7ac5e51b52f281529e81229145c Mon Sep 17 00:00:00 2001 From: kukushking Date: Wed, 5 Feb 2025 22:59:08 +0000 Subject: [PATCH 05/12] add test cases --- .../module-test/deployment-prefix.yaml | 16 ++++ .../mock_deployment_manifest_with_prefix.py | 39 +++++++++ test/unit-test/test_commands_deployment.py | 82 ++++++++++++++++++- test/unit-test/test_commands_stack.py | 21 ++++- 4 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 test/unit-test/mock_data/manifests/module-test/deployment-prefix.yaml create mode 100644 test/unit-test/mock_data/mock_deployment_manifest_with_prefix.py diff --git a/test/unit-test/mock_data/manifests/module-test/deployment-prefix.yaml b/test/unit-test/mock_data/manifests/module-test/deployment-prefix.yaml new file mode 100644 index 00000000..fcdfedc2 --- /dev/null +++ b/test/unit-test/mock_data/manifests/module-test/deployment-prefix.yaml @@ -0,0 +1,16 @@ +name: example-test-dev +toolchainRegion: us-west-2 +groups: + - name: test + path: test/unit-test/mock_data/manifests/module-test/test-modules.yaml +targetAccountMappings: + - alias: primary + accountId: 123456789012 + default: true + rolePrefix: /test1/ + policyPrefix: /test2/ + regionMappings: + - region: us-west-2 + default: true + parametersRegional: + someKey: someValue diff --git a/test/unit-test/mock_data/mock_deployment_manifest_with_prefix.py b/test/unit-test/mock_data/mock_deployment_manifest_with_prefix.py new file mode 100644 index 00000000..7678fc84 --- /dev/null +++ b/test/unit-test/mock_data/mock_deployment_manifest_with_prefix.py @@ -0,0 +1,39 @@ +deployment_manifest = { + "name": "mlops", + "toolchain_region": "us-east-1", + "groups": [ + { + "name": "optionals", + "path": "manifests/mlops/optional-modules.yaml", + "modules": [ + { + "name": "networking", + "path": "modules/optionals/networking/", + "parameters": [{"name": "internet-accessible", "value": True}], + "target_account": "primary", + "target_region": "us-east-1", + }, + ], + }, + ], + "target_account_mappings": [ + { + "alias": "primary", + "account_id": "123456789012", + "default": True, + "parameters_global": { + "dockerCredentialsSecret": "aws-addf-docker-credentials", + "permissionsBoundaryName": "boundary", + }, + "rolePrefix": "/test1/", + "policyPrefix": "/test2/", + "region_mappings": [ + { + "region": "us-east-1", + "default": True, + "parameters_regional": {}, + } + ], + } + ], +} diff --git a/test/unit-test/test_commands_deployment.py b/test/unit-test/test_commands_deployment.py index b101559a..29fdc4c9 100644 --- a/test/unit-test/test_commands_deployment.py +++ b/test/unit-test/test_commands_deployment.py @@ -4,6 +4,7 @@ import mock_data.mock_deployment_manifest_for_destroy as mock_deployment_manifest_for_destroy import mock_data.mock_deployment_manifest_huge as mock_deployment_manifest_huge +import mock_data.mock_deployment_manifest_with_prefix as mock_deployment_manifest_with_prefix import mock_data.mock_deployspec as mock_deployspec import mock_data.mock_manifests as mock_manifests import mock_data.mock_module_info_huge as mock_module_info_huge @@ -80,6 +81,24 @@ def test_apply_violations(session_manager, mocker): dc.apply(deployment_manifest_path="test/unit-test/mock_data/manifests/module-test/deployment-hc.yaml") +@pytest.mark.commands +@pytest.mark.commands_deployment +def test_apply_with_prefix(session_manager, mocker): + mocker.patch("seedfarmer.commands._deployment_commands.write_deployment_manifest", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.prime_target_accounts", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.du.populate_module_info_index", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.du.filter_deploy_destroy", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.write_deployment_manifest", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.du.validate_module_dependencies", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.destroy_deployment", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.deploy_deployment", return_value=None) + dc.apply( + deployment_manifest_path="test/unit-test/mock_data/manifests/module-test/deployment-prefix.yaml", + role_prefix="/test1/", + dryrun=True, + ) + + @pytest.mark.commands @pytest.mark.commands_deployment def test_destroy_clean(session_manager, mocker): @@ -108,6 +127,18 @@ def test_destroy_not_found(session_manager, mocker): dc.destroy(deployment_name="myapp", dryrun=True, remove_seedkit=False) +@pytest.mark.commands +@pytest.mark.commands_deployment +def test_destroy_with_prefix(session_manager, mocker): + mocker.patch( + "seedfarmer.commands._deployment_commands.du.generate_deployed_manifest", + return_value=DeploymentManifest(**mock_deployment_manifest_for_destroy.destroy_manifest), + ) + mocker.patch("seedfarmer.commands._deployment_commands.destroy_deployment", return_value=None) + + dc.destroy(deployment_name="myapp", role_prefix="/test/", dryrun=True, remove_seedkit=False) + + # @pytest.mark.commands # @pytest.mark.commands_deployment # def test_tear_down_target_accounts(session_manager,mocker): @@ -211,6 +242,47 @@ def test_execute_deploy(session_manager, mocker): dc._execute_deploy(mdo) +@pytest.mark.commands +@pytest.mark.commands_deployment +def test_execute_deploy_with_prefix(session_manager, mocker): + mocker.patch("seedfarmer.commands._deployment_commands.load_parameter_values", return_value=None) + mocker.patch( + "seedfarmer.commands._deployment_commands.get_modulestack_path", + return_value="path", + ) + deploy_module_stack_mock = mocker.patch( + "seedfarmer.commands._deployment_commands.commands.deploy_module_stack", + return_value=("stack_name", "role_name"), + ) + mocker.patch("seedfarmer.commands._deployment_commands.get_module_metadata", return_value=None) + mocker.patch( + "seedfarmer.commands._deployment_commands.get_role_arn", + return_value="role_arn", + ) + mocker.patch("seedfarmer.commands._deployment_commands.du.prepare_ssm_for_deploy", return_value=None) + mocker.patch("seedfarmer.commands._deployment_commands.commands.deploy_module", return_value=None) + dep = DeploymentManifest(**mock_deployment_manifest_with_prefix.deployment_manifest) + dep.validate_and_set_module_defaults() + group = dep.groups[0] + module_manifest = group.modules[0] + module_manifest.deploy_spec = DeploySpec(**mock_deployspec.dummy_deployspec) + mdo = ModuleDeployObject(deployment_manifest=dep, group_name=dep.groups[0].name, module_name=module_manifest.name) + dc._execute_deploy(mdo) + + deploy_module_stack_mock.assert_called_with( + module_stack_path="path", + deployment_name="mlops", + group_name="optionals", + module_name="networking", + account_id="123456789012", + region="us-east-1", + parameters=None, + docker_credentials_secret="aws-addf-docker-credentials", + permissions_boundary_arn="arn:aws:iam::123456789012:policy/boundary", + role_prefix="/test1/", + ) + + @pytest.mark.commands @pytest.mark.commands_deployment def test_execute_destroy_invalid_spec(session_manager, mocker): @@ -301,7 +373,11 @@ def test_deploy_deployment(session_manager, mocker): @pytest.mark.commands @pytest.mark.commands_deployment -def test_create_module_deployment_role(session_manager, mocker): +@pytest.mark.parametrize( + ("manifest", "expected_role_prefix"), + [(mock_deployment_manifest_huge, "/"), (mock_deployment_manifest_with_prefix, "/test1/")], +) +def test_create_module_deployment_role(session_manager, mocker, manifest, expected_role_prefix): mocker.patch( "seedfarmer.commands._deployment_commands.get_generic_module_deployment_role_name", return_value="generic-module-deployment-role", @@ -310,7 +386,7 @@ def test_create_module_deployment_role(session_manager, mocker): "seedfarmer.commands._deployment_commands.create_module_deployment_role", return_value=None ) - dep = DeploymentManifest(**mock_deployment_manifest_huge.deployment_manifest) + dep = DeploymentManifest(**manifest.deployment_manifest) dep.validate_and_set_module_defaults() dc.create_generic_module_deployment_role( @@ -325,7 +401,7 @@ def test_create_module_deployment_role(session_manager, mocker): permissions_boundary_arn="arn:aws:iam::123456789012:policy/boundary", docker_credentials_secret=None, session=ANY, - role_prefix="/", + role_prefix=expected_role_prefix, ) diff --git a/test/unit-test/test_commands_stack.py b/test/unit-test/test_commands_stack.py index ecd6e12c..335a19a2 100644 --- a/test/unit-test/test_commands_stack.py +++ b/test/unit-test/test_commands_stack.py @@ -1,4 +1,5 @@ import os +from unittest.mock import ANY import pytest import yaml @@ -149,12 +150,15 @@ def test_deploy_module_stack(session_manager, mocker): @pytest.mark.commands @pytest.mark.commands_stack -def test_create_module_deployment_role(session_manager, mocker): +@pytest.mark.parametrize("role_prefix", [None, "/", "/test/"]) +def test_create_module_deployment_role(session_manager, mocker, role_prefix): mocker.patch( "seedfarmer.commands._stack_commands.services.cfn.does_stack_exist", return_value=(True, {"ProjectPolicyARN": "arn"}), ) - mocker.patch("seedfarmer.commands._stack_commands.iam.create_check_iam_role", return_value=None) + create_iam_role_mock = mocker.patch( + "seedfarmer.commands._stack_commands.iam.create_check_iam_role", return_value=None + ) mocker.patch( "seedfarmer.commands._stack_commands.commands.seedkit_deployed", return_value=(True, "stackname", {"SeedkitResourcesPolicyArn": "arn"}), @@ -171,6 +175,19 @@ def test_create_module_deployment_role(session_manager, mocker): group_name="group", module_name="module", docker_credentials_secret="fsfasdfsad", + role_prefix=role_prefix, + ) + + create_iam_role_mock.assert_called_once_with( + project_name="myapp", + deployment_name="myapp", + group_name="group", + module_name="module", + trust_policy=ANY, + role_name="module-deployment-role", + permissions_boundary_arn=None, + session=ANY, + role_prefix=role_prefix, ) From fa834738aad157af6e8627efdd2388a728956b28 Mon Sep 17 00:00:00 2001 From: kukushking Date: Thu, 6 Feb 2025 00:41:21 +0000 Subject: [PATCH 06/12] docs --- docs/source/bootstrapping.md | 8 ++++++++ docs/source/manifests.md | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/source/bootstrapping.md b/docs/source/bootstrapping.md index 051a9e4e..129fc7b5 100644 --- a/docs/source/bootstrapping.md +++ b/docs/source/bootstrapping.md @@ -26,6 +26,8 @@ Options: --region TEXT AWS region to use --qualifier TEXT A qualifier to append to toolchain role (alpha-numeric char max length of 6) + --role-prefix TEXT IAM Role Path prefix. + --policy-prefix TEXT IAM Policy Path prefix. -pa, --policy-arn TEXT ARN of existing Policy to attach to Target Role (Deploymenmt Role) This can be use multiple times, but EACH policy MUST be @@ -65,6 +67,7 @@ Options: --region TEXT AWS region to use --qualifier TEXT A qualifier to append to target role (alpha- numeric char max length of 6) + --role-prefix TEXT IAM Role Path prefix. -pa, --policy-arn TEXT ARN of existing Policy to attach to Target Role (Deploymenmt Role) This can be use multiple times to create a list, but EACH @@ -84,6 +87,11 @@ We have added support for the use of a qualifier for the toolchain role and the The qualifier post-pends a 6 chars alpha-numeric string to the deployment role and toolchain role. The qualifier **MUST BE THE SAME ON THE TOOLCHAIN ROLE AND EACH TARGET ROLE.** +## IAM Paths Prefixes for Toolchain, Target Roles, and Policies +We have added support for the use of a IAM Paths for the toolchain role, target account deployment role(s), and policie(s). Using IAM Paths you can create groupings and design a logical separation to simplify permissions management. A common example in organizations is using Service Control Policies enforcing logical separation by team e.g. `/legal/` or `/sales/`, or project name. + +A `--role-prefix` and `--policy-prefix` can be used if you want to provide IAM Paths to the roles and policies created by `seed-farmer`. IAM Paths must begin and end with a `/`. More information in [IAM identifies](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). + ## Prepping the Account / Region `seedfarmer` leverages the AWS CDKv2. This must be bootstrapped in each account/region combination to be used of each target account. diff --git a/docs/source/manifests.md b/docs/source/manifests.md index 5bbc44d4..00216bdd 100644 --- a/docs/source/manifests.md +++ b/docs/source/manifests.md @@ -34,6 +34,8 @@ targetAccountMappings: npmMirrorSecret: /something/aws-addf-mirror-credentials pypiMirror: https://pypi.python.org/simple pypiMirrorSecret: /something/aws-addf-mirror-mirror-credentials + rolePrefix: / + policyPrefix: / parametersGlobal: dockerCredentialsSecret: nameofsecret permissionsBoundaryName: policyname @@ -108,6 +110,8 @@ targetAccountMappings: - **npmMirrorSecret** - the AWS SecretManager to use when setting the mirror (see [Mirror Override](mirroroverride)) - **pypiMirror** - the Pypi mirror to use (see [Mirror Override](mirroroverride)) - **pypiMirrorSecret** - the AWS SecretManager to use when setting the mirror (see [Mirror Override](mirroroverride)) + - **rolePrefix** - IAM path prefix to use with seedfarmer roles (see [IAM Path Prefixes](iamprefixes)) + - **policyPrefix** - IAM path prefix to use with seedfarmer policies (see [IAM Path Prefixes](iamprefixes)) - **parametersGlobal** - these are parameters that apply to all region mappings unless otherwise overridden at the region level - **dockerCredentialsSecret** - please see [Docker Credentials Secret](dockerCredentialsSecret) - **permissionsBoundaryName** - the name of the [permissions boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) policy to apply to all module-specific roles created @@ -531,6 +535,12 @@ This would result in the creation of an `_auth` entry in npm config (`.npmrc`) w npm config set //the-mirror-dns/npm/:_auth="mybase64encodedssltoken" ``` +(iamprefixes)= +## IAM Path Prefixes +Using IAM Paths you can create groupings and design a logical separation to simplify permissions management. A common example in organizations is using Service Control Policies enforcing logical separation by team e.g. `/legal/` or `/sales/`, or project name. + +It is possible to override the default paths of `/` for `seed-farmer` IAM roles and policies using the deployment manifest using `rolePrefix` and `policyPrefix` at the account/region level. IAM Paths must begin and end with a `/`. More information in [IAM identifies](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). + (archivesecret)= ### Archive Secret From 88a7af465a28ba510a310ec59ba108ab9374e1d9 Mon Sep 17 00:00:00 2001 From: kukushking Date: Thu, 6 Feb 2025 11:23:47 +0000 Subject: [PATCH 07/12] add permissions boundary --- seedfarmer/commands/_deployment_commands.py | 17 +++++++++++++---- seedfarmer/commands/_stack_commands.py | 8 ++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/seedfarmer/commands/_deployment_commands.py b/seedfarmer/commands/_deployment_commands.py index 27a37cd7..8b71b1ad 100644 --- a/seedfarmer/commands/_deployment_commands.py +++ b/seedfarmer/commands/_deployment_commands.py @@ -431,13 +431,22 @@ def _prime_accounts(args: Dict[str, Any]) -> List[Any]: params = [] for target_account_region in deployment_manifest.target_accounts_regions: + account_id = target_account_region["account_id"] + region = target_account_region["region"] + role_prefix = target_account_region["role_prefix"] + policy_prefix = target_account_region["policy_prefix"] + permissions_boundary_arn = deployment_manifest.get_permission_boundary_arn( + target_account=account_id, + target_region=region, + ) param_d = { - "account_id": target_account_region["account_id"], - "region": target_account_region["region"], + "account_id": account_id, + "region": region, "update_seedkit": update_seedkit, "update_project_policy": update_project_policy, - "role_prefix": target_account_region["role_prefix"], - "policy_prefix": target_account_region["policy_prefix"], + "role_prefix": role_prefix, + "policy_prefix": policy_prefix, + "permissions_boundary_arn": permissions_boundary_arn, } if target_account_region["network"] is not None: diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py index 83722a68..189a991b 100644 --- a/seedfarmer/commands/_stack_commands.py +++ b/seedfarmer/commands/_stack_commands.py @@ -591,6 +591,7 @@ def deploy_seedkit( update_seedkit: Optional[bool] = False, role_prefix: str = "/", policy_prefix: str = "/", + permissions_boundary_arn: Optional[str] = None, **kwargs: Any, ) -> Dict[str, Any]: """ @@ -609,6 +610,12 @@ def deploy_seedkit( The Subnet IDs to associate seedkit with (codebuild) security_group_ids: Optional[List[str]] The Security Group IDs to associate seedkit with (codebuild) + role_prefix: Optional[str] + The IAM Path Prefix to use for seedkit role + policy_prefix: Optional[str] + The IAM Path Prefix to use for seedkit policy + permissions_boundary_arn: Optional[str] + The ARN of the permissions boundary to attach to seedkit role """ session = SessionManager().get_or_create().get_deployment_session(account_id=account_id, region_name=region) stack_exists, _, stack_outputs = commands.seedkit_deployed(seedkit_name=config.PROJECT, session=session) @@ -623,6 +630,7 @@ def deploy_seedkit( kwargs = { "role_prefix": role_prefix, "policy_prefix": policy_prefix, + "permissions_boundary_arn": permissions_boundary_arn } commands.deploy_seedkit( From bc630ca5895c4c9be9e5450228af3e4ffb202304 Mon Sep 17 00:00:00 2001 From: kukushking Date: Thu, 6 Feb 2025 11:34:19 +0000 Subject: [PATCH 08/12] static checks --- seedfarmer/commands/_stack_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py index 189a991b..6eab778d 100644 --- a/seedfarmer/commands/_stack_commands.py +++ b/seedfarmer/commands/_stack_commands.py @@ -630,7 +630,7 @@ def deploy_seedkit( kwargs = { "role_prefix": role_prefix, "policy_prefix": policy_prefix, - "permissions_boundary_arn": permissions_boundary_arn + "permissions_boundary_arn": permissions_boundary_arn, } commands.deploy_seedkit( From 4dc53921fff2929df84c442806ee24684b280c17 Mon Sep 17 00:00:00 2001 From: kukushking Date: Thu, 6 Feb 2025 14:03:34 +0000 Subject: [PATCH 09/12] add region mapping --- docs/source/bootstrapping.md | 2 +- docs/source/manifests.md | 11 +++- seedfarmer/commands/_deployment_commands.py | 16 ++--- seedfarmer/commands/_stack_commands.py | 33 +++++----- .../models/manifests/_deployment_manifest.py | 61 +++++++++++++++---- seedfarmer/resources/deployment_role.template | 11 ++-- seedfarmer/resources/toolchain_role.template | 3 +- 7 files changed, 89 insertions(+), 48 deletions(-) diff --git a/docs/source/bootstrapping.md b/docs/source/bootstrapping.md index 129fc7b5..9c3f1ef9 100644 --- a/docs/source/bootstrapping.md +++ b/docs/source/bootstrapping.md @@ -90,7 +90,7 @@ The qualifier post-pends a 6 chars alpha-numeric string to the deployment role a ## IAM Paths Prefixes for Toolchain, Target Roles, and Policies We have added support for the use of a IAM Paths for the toolchain role, target account deployment role(s), and policie(s). Using IAM Paths you can create groupings and design a logical separation to simplify permissions management. A common example in organizations is using Service Control Policies enforcing logical separation by team e.g. `/legal/` or `/sales/`, or project name. -A `--role-prefix` and `--policy-prefix` can be used if you want to provide IAM Paths to the roles and policies created by `seed-farmer`. IAM Paths must begin and end with a `/`. More information in [IAM identifies](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). +A `--role-prefix` and `--policy-prefix` can be used if you want to provide IAM Paths to the roles and policies created by `seed-farmer`. IAM Paths must begin and end with a `/`. More information in [IAM identifiers](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). ## Prepping the Account / Region `seedfarmer` leverages the AWS CDKv2. This must be bootstrapped in each account/region combination to be used of each target account. diff --git a/docs/source/manifests.md b/docs/source/manifests.md index 00216bdd..8844ef52 100644 --- a/docs/source/manifests.md +++ b/docs/source/manifests.md @@ -122,6 +122,8 @@ targetAccountMappings: - **npmMirror** - the NPM registry mirror to use (see [Mirror Override](mirroroverride)) - **pypiMirror** - the Pypi mirror to use (see [Mirror Override](mirroroverride)) - **pypiMirrorSecret** - the AWS SecretManager to use when setting the mirror (see [Mirror Override](mirroroverride)) + - **rolePrefix** - IAM path prefix to use with seedfarmer roles (see [IAM Path Prefixes](iamprefixes)) + - **policyPrefix** - IAM path prefix to use with seedfarmer policies (see [IAM Path Prefixes](iamprefixes)) - **parametersRegional** - these are parameters that apply to all region mappings unless otherwise overridden at the region level - **dockerCredentialsSecret** - please see [Docker Credentials Secret](dockerCredentialsSecret) - This is a NAMED PARAMETER...in that `dockerCredentialsSecret` is recognized by `seed-farmer` @@ -539,7 +541,14 @@ npm config set //the-mirror-dns/npm/:_auth="mybase64encodedssltoken" ## IAM Path Prefixes Using IAM Paths you can create groupings and design a logical separation to simplify permissions management. A common example in organizations is using Service Control Policies enforcing logical separation by team e.g. `/legal/` or `/sales/`, or project name. -It is possible to override the default paths of `/` for `seed-farmer` IAM roles and policies using the deployment manifest using `rolePrefix` and `policyPrefix` at the account/region level. IAM Paths must begin and end with a `/`. More information in [IAM identifies](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). +It is possible to override the default paths of `/` for `seed-farmer` IAM roles and policies using the deployment manifest using `rolePrefix` and `policyPrefix` at the account/region level. IAM Paths must begin and end with a `/`. More information in [IAM identifiers](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). + +There is a level of logic that is followed: +1. if a prefix is defined at the region level --- USE IT... ELSE +2. if a prefix is defined at the account level --- USE IT... ELSE +4. use default `/` prefix + +NOTE: the prefixes provided must match the prefixes provided during bootstrap, unless a custom bootstrap is used. (archivesecret)= ### Archive Secret diff --git a/seedfarmer/commands/_deployment_commands.py b/seedfarmer/commands/_deployment_commands.py index 8b71b1ad..b965ab1f 100644 --- a/seedfarmer/commands/_deployment_commands.py +++ b/seedfarmer/commands/_deployment_commands.py @@ -119,10 +119,6 @@ def create_generic_module_deployment_role( deployment_name=cast(str, deployment_manifest.name), region=region, ) - target_account_mapping = deployment_manifest.get_target_account_mapping(account_id=account_id) - role_prefix = ( - target_account_mapping.role_prefix if target_account_mapping and target_account_mapping.role_prefix else "/" - ) create_module_deployment_role( role_name=role_name, deployment_name=cast(str, deployment_manifest.name), @@ -136,7 +132,7 @@ def create_generic_module_deployment_role( region=region, ), session=session, - role_prefix=role_prefix, + role_prefix=deployment_manifest.get_account_region_role_prefix(account_id=account_id, region=region), ) return role_name @@ -192,10 +188,7 @@ def _execute_deploy( deployment_name=cast(str, mdo.deployment_manifest.name), region=region, ) - target_account_mapping = mdo.deployment_manifest.get_target_account_mapping(account_id=account_id) - role_prefix = ( - target_account_mapping.role_prefix if target_account_mapping and target_account_mapping.role_prefix else "/" - ) + role_prefix = mdo.deployment_manifest.get_account_region_role_prefix(account_id=account_id, region=region) if module_stack_path: _, module_role_name = commands.deploy_module_stack( @@ -251,9 +244,8 @@ def _execute_destroy(mdo: ModuleDeployObject) -> Optional[ModuleDeploymentRespon target_account_id = cast(str, module_manifest.get_target_account_id()) target_region = cast(str, module_manifest.target_region) - target_account_mapping = mdo.deployment_manifest.get_target_account_mapping(account_id=target_account_id) - role_prefix = ( - target_account_mapping.role_prefix if target_account_mapping and target_account_mapping.role_prefix else "/" + role_prefix = mdo.deployment_manifest.get_account_region_role_prefix( + account_id=target_account_id, region=target_region ) session = ( SessionManager() diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py index 6eab778d..05efff25 100644 --- a/seedfarmer/commands/_stack_commands.py +++ b/seedfarmer/commands/_stack_commands.py @@ -625,23 +625,24 @@ def deploy_seedkit( else: _logger.debug("Initializing / Updating SeedKit for Account/Region: %s/%s", account_id, region) - kwargs = {} + seedkit_args = { + "seedkit_name": config.PROJECT, + "deploy_codeartifact": deploy_codeartifact, + "session": session, + "vpc_id": vpc_id, + "subnet_ids": private_subnet_ids, + "security_group_ids": security_group_ids, + } + if version.parse(cs.__version__) >= version.parse("1.3.0"): - kwargs = { - "role_prefix": role_prefix, - "policy_prefix": policy_prefix, - "permissions_boundary_arn": permissions_boundary_arn, - } - - commands.deploy_seedkit( - seedkit_name=config.PROJECT, - deploy_codeartifact=deploy_codeartifact, - session=session, - vpc_id=vpc_id, - subnet_ids=private_subnet_ids, - security_group_ids=security_group_ids, - **kwargs, - ) + if role_prefix: + seedkit_args["role_prefix"] = role_prefix + if policy_prefix: + seedkit_args["policy_prefix"] = policy_prefix + if permissions_boundary_arn: + seedkit_args["permissions_boundary_arn"] = permissions_boundary_arn + + commands.deploy_seedkit(**seedkit_args) # Go get the outputs and return them _, _, stack_outputs = commands.seedkit_deployed(seedkit_name=config.PROJECT, session=session) return dict(stack_outputs) diff --git a/seedfarmer/models/manifests/_deployment_manifest.py b/seedfarmer/models/manifests/_deployment_manifest.py index 760267b9..c12c7619 100644 --- a/seedfarmer/models/manifests/_deployment_manifest.py +++ b/seedfarmer/models/manifests/_deployment_manifest.py @@ -63,6 +63,8 @@ class RegionMapping(CamelModel): npm_mirror_secret: Optional[str] = None seedkit_metadata: Optional[Dict[str, Any]] = None seedfarmer_artifact_bucket: Optional[str] = None + role_prefix: Optional[str] = None + policy_prefix: Optional[str] = None class TargetAccountMapping(CamelModel): @@ -232,18 +234,23 @@ def target_accounts_regions(self) -> List[Dict[str, str]]: self._accounts_regions = [] for target_account in self.target_account_mappings: for region in target_account.region_mappings: - self._accounts_regions.append( - { - "alias": target_account.alias, - "account_id": target_account.actual_account_id, - "region": region.region, - "network": region.network, # type: ignore - "parameters_regional": region.parameters_regional, # type: ignore - "codebuild_image": cast(str, region.codebuild_image), - "role_prefix": target_account.role_prefix, # type: ignore - "policy_prefix": target_account.policy_prefix, # type: ignore - } - ) + account_region_args = { + "alias": target_account.alias, + "account_id": target_account.actual_account_id, + "region": region.region, + "network": region.network, + "parameters_regional": region.parameters_regional, + "codebuild_image": cast(str, region.codebuild_image), + } + role_prefix = region.role_prefix if region.role_prefix else target_account.role_prefix + policy_prefix = region.policy_prefix if region.policy_prefix else target_account.policy_prefix + + if role_prefix: + account_region_args["role_prefix"] = role_prefix + if policy_prefix: + account_region_args["policy_prefix"] = policy_prefix + + self._accounts_regions.append(account_region_args) # type: ignore return self._accounts_regions def get_parameter_value( @@ -449,6 +456,36 @@ def get_region_seedfarmer_bucket( else: return None + def get_account_region_role_prefix( + self, + *, + account_alias: Optional[str] = None, + account_id: Optional[str] = None, + region: Optional[str] = None, + ) -> str: + if account_alias is not None and account_id is not None: + raise seedfarmer.errors.InvalidManifestError("Only one of 'account_alias' and 'account_id' is allowed") + + use_default_account = account_alias is None and account_id is None + use_default_region = region is None + default_prefix = "/" + for target_account in self.target_account_mappings: + if ( + account_alias == target_account.alias + or account_id == target_account.actual_account_id + or (use_default_account and target_account.default) + ): + for region_mapping in target_account.region_mappings: + if region == region_mapping.region or (use_default_region and region_mapping.default): + role_prefix = ( + region_mapping.role_prefix + if region_mapping.role_prefix is not None + else target_account.role_prefix + ) + return role_prefix if role_prefix else default_prefix + else: + return default_prefix + def get_permission_boundary_arn(self, target_account: str, target_region: str) -> Optional[str]: permissions_boundary_name = self.get_parameter_value( "permissionsBoundaryName", diff --git a/seedfarmer/resources/deployment_role.template b/seedfarmer/resources/deployment_role.template index 160bd553..79710975 100644 --- a/seedfarmer/resources/deployment_role.template +++ b/seedfarmer/resources/deployment_role.template @@ -64,14 +64,14 @@ Resources: - iam:List* Effect: Allow Resource: - - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role{{ role_prefix }}{{ project_name }}-*" - - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role{{ role_prefix }}codeseeder-{{ project_name }}-*" - - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy{{ policy_prefix }}{{ project_name }}-*" - - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy{{ policy_prefix }}codeseeder-{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/codeseeder-{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/{{ project_name }}-*" - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/codeseeder-{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*/{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*/codeseeder-{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/*/{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/*/codeseeder-{{ project_name }}-*" Sid: DeploymentIAM - Action: - codebuild:Update* @@ -120,7 +120,8 @@ Resources: - sts:GetSessionToken Effect: Allow Resource: - - Fn::Sub: "arn:${AWS::Partition}:iam::*:role{{ role_prefix }}{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::*:role/{{ project_name }}-*" + - Fn::Sub: "arn:${AWS::Partition}:iam::*:role/*/{{ project_name }}-*" Sid: DeploymentSTS - Action: - ssm:Put* diff --git a/seedfarmer/resources/toolchain_role.template b/seedfarmer/resources/toolchain_role.template index 9439dbbc..f5320fed 100644 --- a/seedfarmer/resources/toolchain_role.template +++ b/seedfarmer/resources/toolchain_role.template @@ -25,7 +25,8 @@ Resources: - sts:GetSessionToken Effect: Allow Resource: - - Fn::Sub: "arn:${AWS::Partition}:iam::*:role{{ role_prefix }}seedfarmer-{{ project_name }}*" + - Fn::Sub: "arn:${AWS::Partition}:iam::*:role/seedfarmer-{{ project_name }}*" + - Fn::Sub: "arn:${AWS::Partition}:iam::*:role/*/seedfarmer-{{ project_name }}*" Sid: ToolChainSTS - Action: - ssm:Put* From 345cfc2d498ed3d0212f210949bb935753f40c6d Mon Sep 17 00:00:00 2001 From: kukushking Date: Thu, 6 Feb 2025 14:53:34 +0000 Subject: [PATCH 10/12] cli docstrings --- seedfarmer/cli_groups/_bootstrap_group.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seedfarmer/cli_groups/_bootstrap_group.py b/seedfarmer/cli_groups/_bootstrap_group.py index 6bad5956..c53cb5be 100644 --- a/seedfarmer/cli_groups/_bootstrap_group.py +++ b/seedfarmer/cli_groups/_bootstrap_group.py @@ -104,13 +104,13 @@ def bootstrap() -> None: @click.option( "--role-prefix", default="/", - help="IAM Role Path prefix.", + help="An IAM path prefix to use with the seedfarmer roles.", required=False, ) @click.option( "--policy-prefix", default="/", - help="IAM Policy Path prefix.", + help="An IAM path prefix to use with the seedfarmer policies.", required=False, ) @click.option( @@ -216,7 +216,7 @@ def bootstrap_toolchain( @click.option( "--role-prefix", default="/", - help="IAM Role Path prefix.", + help="An IAM path prefix to use with the seedfarmer roles.", required=False, ) @click.option( From 25d2bae4af0e281ddf918d3e1a24fffe85932c65 Mon Sep 17 00:00:00 2001 From: kukushking Date: Thu, 6 Feb 2025 14:57:05 +0000 Subject: [PATCH 11/12] bootstrapping.md --- docs/source/bootstrapping.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/bootstrapping.md b/docs/source/bootstrapping.md index 9c3f1ef9..be74599a 100644 --- a/docs/source/bootstrapping.md +++ b/docs/source/bootstrapping.md @@ -26,8 +26,10 @@ Options: --region TEXT AWS region to use --qualifier TEXT A qualifier to append to toolchain role (alpha-numeric char max length of 6) - --role-prefix TEXT IAM Role Path prefix. - --policy-prefix TEXT IAM Policy Path prefix. + --role-prefix TEXT An IAM path prefix to use with the + seedfarmer roles. + --policy-prefix TEXT An IAM path prefix to use with the + seedfarmer policies. -pa, --policy-arn TEXT ARN of existing Policy to attach to Target Role (Deploymenmt Role) This can be use multiple times, but EACH policy MUST be @@ -67,7 +69,8 @@ Options: --region TEXT AWS region to use --qualifier TEXT A qualifier to append to target role (alpha- numeric char max length of 6) - --role-prefix TEXT IAM Role Path prefix. + --role-prefix TEXT An IAM path prefix to use with the seedfarmer + roles. -pa, --policy-arn TEXT ARN of existing Policy to attach to Target Role (Deploymenmt Role) This can be use multiple times to create a list, but EACH From f0cfcd468a8dcb4f5d3ab5b35b807b4ccf02216f Mon Sep 17 00:00:00 2001 From: kukushking Date: Sun, 9 Feb 2025 01:48:29 +0000 Subject: [PATCH 12/12] refactor to use paths at the edges --- seedfarmer/__main__.py | 8 +++---- seedfarmer/cli_groups/_bootstrap_group.py | 12 +++++------ seedfarmer/commands/_bootstrap_commands.py | 21 +++++++++++-------- seedfarmer/commands/_deployment_commands.py | 14 ++++++++----- seedfarmer/commands/_stack_commands.py | 8 +++---- seedfarmer/mgmt/deploy_utils.py | 2 +- .../models/manifests/_deployment_manifest.py | 12 ++++------- seedfarmer/services/_iam.py | 4 ++-- seedfarmer/services/session_manager.py | 10 ++++----- seedfarmer/utils.py | 6 ++++-- 10 files changed, 51 insertions(+), 46 deletions(-) diff --git a/seedfarmer/__main__.py b/seedfarmer/__main__.py index ba2689a2..30552e6b 100644 --- a/seedfarmer/__main__.py +++ b/seedfarmer/__main__.py @@ -66,7 +66,7 @@ def version() -> None: ) @click.option( "--role-prefix", - default="/", + default=None, help="""An IAM path prefix to use with the seedfarmer roles. Use only if bootstrapped with this path""", required=False, @@ -136,7 +136,7 @@ def apply( profile: Optional[str], region: Optional[str], qualifier: Optional[str], - role_prefix: str, + role_prefix: Optional[str], env_files: List[str], debug: bool, dry_run: bool, @@ -211,7 +211,7 @@ def apply( ) @click.option( "--role-prefix", - default="/", + default=None, help="""An IAM path prefix to use with the seedfarmer roles. Use only if bootstrapped with this path""", required=False, @@ -264,7 +264,7 @@ def destroy( profile: Optional[str], region: Optional[str], qualifier: Optional[str], - role_prefix: str, + role_prefix: Optional[str], env_files: List[str], debug: bool, enable_session_timeout: bool, diff --git a/seedfarmer/cli_groups/_bootstrap_group.py b/seedfarmer/cli_groups/_bootstrap_group.py index c53cb5be..017b6383 100644 --- a/seedfarmer/cli_groups/_bootstrap_group.py +++ b/seedfarmer/cli_groups/_bootstrap_group.py @@ -103,13 +103,13 @@ def bootstrap() -> None: ) @click.option( "--role-prefix", - default="/", + default=None, help="An IAM path prefix to use with the seedfarmer roles.", required=False, ) @click.option( "--policy-prefix", - default="/", + default=None, help="An IAM path prefix to use with the seedfarmer policies.", required=False, ) @@ -133,8 +133,8 @@ def bootstrap_toolchain( profile: Optional[str], region: Optional[str], qualifier: Optional[str], - role_prefix: str, - policy_prefix: str, + role_prefix: Optional[str], + policy_prefix: Optional[str], as_target: bool, synth: bool, debug: bool, @@ -215,7 +215,7 @@ def bootstrap_toolchain( ) @click.option( "--role-prefix", - default="/", + default=None, help="An IAM path prefix to use with the seedfarmer roles.", required=False, ) @@ -237,7 +237,7 @@ def bootstrap_target( profile: Optional[str], region: Optional[str], qualifier: Optional[str], - role_prefix: str, + role_prefix: Optional[str], synth: bool, debug: bool, ) -> None: diff --git a/seedfarmer/commands/_bootstrap_commands.py b/seedfarmer/commands/_bootstrap_commands.py index a033136d..511c5da3 100644 --- a/seedfarmer/commands/_bootstrap_commands.py +++ b/seedfarmer/commands/_bootstrap_commands.py @@ -39,7 +39,7 @@ def get_toolchain_template( principal_arn: List[str], role_name: str, permissions_boundary_arn: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, ) -> Dict[Any, Any]: with open((os.path.join(CLI_ROOT, "resources/toolchain_role.template")), "r") as f: role = yaml.safe_load(f) @@ -49,6 +49,7 @@ def get_toolchain_template( ] = principal_arn if permissions_boundary_arn: role["Resources"]["ToolchainRole"]["Properties"]["PermissionsBoundary"] = permissions_boundary_arn + role_prefix = role_prefix if role_prefix else "/" template = Template(json.dumps(role)) t = template.render( { @@ -67,8 +68,8 @@ def get_deployment_template( role_name: str, policy_arns: Optional[List[str]], permissions_boundary_arn: Optional[str] = None, - role_prefix: str = "/", - policy_prefix: str = "/", + role_prefix: Optional[str] = None, + policy_prefix: Optional[str] = None, ) -> Dict[Any, Any]: with open((os.path.join(CLI_ROOT, "resources/deployment_role.template")), "r") as f: role = yaml.safe_load(f) @@ -76,6 +77,8 @@ def get_deployment_template( role["Resources"]["DeploymentRole"]["Properties"]["PermissionsBoundary"] = permissions_boundary_arn if policy_arns: role["Resources"]["DeploymentRole"]["Properties"]["ManagedPolicyArns"] = policy_arns + role_prefix = role_prefix if role_prefix else "/" + policy_prefix = policy_prefix if policy_prefix else "/" template = Template(json.dumps(role)) t = template.render( { @@ -101,8 +104,8 @@ def bootstrap_toolchain_account( permissions_boundary_arn: Optional[str] = None, policy_arns: Optional[List[str]] = None, qualifier: Optional[str] = None, - role_prefix: str = "/", - policy_prefix: str = "/", + role_prefix: Optional[str] = None, + policy_prefix: Optional[str] = None, profile: Optional[str] = None, region_name: Optional[str] = None, synthesize: bool = False, @@ -126,7 +129,7 @@ def bootstrap_toolchain_account( _logger.debug((json.dumps(template, indent=4))) if not synthesize: session = create_new_session(profile=profile, region_name=region_name) - session_account_id, session_role_arn, partition = get_sts_identity_info(session=session) + session_account_id, _, _ = get_sts_identity_info(session=session) apply_deploy_logic( template=template, role_name=role_stack_name, @@ -169,8 +172,8 @@ def bootstrap_target_account( project_name: str, permissions_boundary_arn: Optional[str] = None, qualifier: Optional[str] = None, - role_prefix: str = "/", - policy_prefix: str = "/", + role_prefix: Optional[str] = None, + policy_prefix: Optional[str] = None, profile: Optional[str] = None, region_name: Optional[str] = None, session: Optional[Session] = None, @@ -182,7 +185,7 @@ def bootstrap_target_account( if not session: session = create_new_session(profile=profile, region_name=region_name) - session_account_id, session_role_arn, partition = get_sts_identity_info(session=session) + session_account_id, _, partition = get_sts_identity_info(session=session) role_stack_name = get_deployment_role_name(project_name=project_name, qualifier=cast(str, qualifier)) toolchain_role_arn = get_toolchain_role_arn( diff --git a/seedfarmer/commands/_deployment_commands.py b/seedfarmer/commands/_deployment_commands.py index b965ab1f..09e0755f 100644 --- a/seedfarmer/commands/_deployment_commands.py +++ b/seedfarmer/commands/_deployment_commands.py @@ -436,11 +436,12 @@ def _prime_accounts(args: Dict[str, Any]) -> List[Any]: "region": region, "update_seedkit": update_seedkit, "update_project_policy": update_project_policy, - "role_prefix": role_prefix, - "policy_prefix": policy_prefix, "permissions_boundary_arn": permissions_boundary_arn, } - + if role_prefix: + param_d["role_prefix"] = role_prefix + if policy_prefix: + param_d["policy_prefix"] = policy_prefix if target_account_region["network"] is not None: network = commands.load_network_values( cast(NetworkMapping, target_account_region["network"]), @@ -765,7 +766,7 @@ def apply( profile: Optional[str] = None, region_name: Optional[str] = None, qualifier: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, dryrun: bool = False, show_manifest: bool = False, enable_session_timeout: bool = False, @@ -914,7 +915,7 @@ def destroy( profile: Optional[str] = None, region_name: Optional[str] = None, qualifier: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, dryrun: bool = False, show_manifest: bool = False, remove_seedkit: bool = False, @@ -936,6 +937,9 @@ def destroy( qualifier : str, optional Any qualifier on the name of toolchain role Defaults to None + role_prefix : str, optional + IAM path prefix on the ARN of the toolchain and deployment roles + Defaults to '/' dryrun : bool, optional This flag indicates that the deployment WILL NOT enact any deployment changes. diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py index 05efff25..e7d5954b 100644 --- a/seedfarmer/commands/_stack_commands.py +++ b/seedfarmer/commands/_stack_commands.py @@ -132,7 +132,7 @@ def create_module_deployment_role( docker_credentials_secret: Optional[str] = None, permissions_boundary_arn: Optional[str] = None, session: Optional[boto3.Session] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, ) -> None: iam.create_check_iam_role( project_name=config.PROJECT, @@ -442,7 +442,7 @@ def deploy_module_stack( parameters: List[ModuleParameter], docker_credentials_secret: Optional[str] = None, permissions_boundary_arn: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, ) -> Tuple[str, str]: """ deploy_module_stack @@ -589,8 +589,8 @@ def deploy_seedkit( private_subnet_ids: Optional[List[str]] = None, security_group_ids: Optional[List[str]] = None, update_seedkit: Optional[bool] = False, - role_prefix: str = "/", - policy_prefix: str = "/", + role_prefix: Optional[str] = None, + policy_prefix: Optional[str] = None, permissions_boundary_arn: Optional[str] = None, **kwargs: Any, ) -> Dict[str, Any]: diff --git a/seedfarmer/mgmt/deploy_utils.py b/seedfarmer/mgmt/deploy_utils.py index 26ccf466..9971cbdc 100644 --- a/seedfarmer/mgmt/deploy_utils.py +++ b/seedfarmer/mgmt/deploy_utils.py @@ -118,7 +118,7 @@ def _get_module_info(args: Dict[str, Any]) -> None: { "account_id": target_account_region["account_id"], "region": target_account_region["region"], - "role_prefix": target_account_region.get("role_prefix", "/"), + "role_prefix": target_account_region["role_prefix"], } for target_account_region in deployment_manifest.target_accounts_regions ] diff --git a/seedfarmer/models/manifests/_deployment_manifest.py b/seedfarmer/models/manifests/_deployment_manifest.py index c12c7619..3cc5c5fd 100644 --- a/seedfarmer/models/manifests/_deployment_manifest.py +++ b/seedfarmer/models/manifests/_deployment_manifest.py @@ -234,6 +234,8 @@ def target_accounts_regions(self) -> List[Dict[str, str]]: self._accounts_regions = [] for target_account in self.target_account_mappings: for region in target_account.region_mappings: + role_prefix = region.role_prefix if region.role_prefix else target_account.role_prefix + policy_prefix = region.policy_prefix if region.policy_prefix else target_account.policy_prefix account_region_args = { "alias": target_account.alias, "account_id": target_account.actual_account_id, @@ -241,15 +243,9 @@ def target_accounts_regions(self) -> List[Dict[str, str]]: "network": region.network, "parameters_regional": region.parameters_regional, "codebuild_image": cast(str, region.codebuild_image), + "role_prefix": role_prefix, + "policy_prefix": policy_prefix, } - role_prefix = region.role_prefix if region.role_prefix else target_account.role_prefix - policy_prefix = region.policy_prefix if region.policy_prefix else target_account.policy_prefix - - if role_prefix: - account_region_args["role_prefix"] = role_prefix - if policy_prefix: - account_region_args["policy_prefix"] = policy_prefix - self._accounts_regions.append(account_region_args) # type: ignore return self._accounts_regions diff --git a/seedfarmer/services/_iam.py b/seedfarmer/services/_iam.py index bfc6f094..1a5780f7 100644 --- a/seedfarmer/services/_iam.py +++ b/seedfarmer/services/_iam.py @@ -52,7 +52,7 @@ def create_check_iam_role( group_name: Optional[str] = None, module_name: Optional[str] = None, session: Optional[Session] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, ) -> None: _logger.debug("Creating IAM Role with name: %s ", role_name) iam_client = boto3_client("iam", session=session) @@ -61,7 +61,7 @@ def create_check_iam_role( except iam_client.exceptions.NoSuchEntityException: args: Dict[str, Any] = { "RoleName": role_name, - "Path": role_prefix, + "Path": role_prefix if role_prefix else "/", "AssumeRolePolicyDocument": json.dumps(trust_policy), "Description": f"deployment-role for {role_name}", "Tags": [ diff --git a/seedfarmer/services/session_manager.py b/seedfarmer/services/session_manager.py index b856925c..5293ec81 100644 --- a/seedfarmer/services/session_manager.py +++ b/seedfarmer/services/session_manager.py @@ -49,7 +49,7 @@ def get_or_create( region_name: Optional[str] = None, toolchain_region: Optional[str] = None, qualifier: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, profile: Optional[str] = None, enable_reaper: bool = False, **kwargs: Optional[Any], @@ -87,7 +87,7 @@ def get_or_create( profile: Optional[str] = None, toolchain_region: Optional[str] = None, qualifier: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, reaper_interval: Optional[int] = None, enable_reaper: bool = False, **kwargs: Optional[Any], @@ -102,7 +102,7 @@ def get_or_create( self.config["profile"] = profile self.config["toolchain_region"] = toolchain_region self.config["qualifier"] = qualifier if qualifier else None - self.config["role_prefix"] = role_prefix + self.config["role_prefix"] = role_prefix if role_prefix else "/" self.config = {**self.config, **kwargs} self.toolchain_role_name = get_toolchain_role_name(project_name, cast(str, qualifier)) @@ -143,7 +143,7 @@ def get_deployment_session(self, account_id: str, region_name: str) -> Session: session_key = f"{account_id}-{region_name}" project_name = self.config["project_name"] qualifier = self.config.get("qualifier") if self.config.get("qualifier") else None - role_prefix = self.config.get("role_prefix", "/") + role_prefix = self.config.get("role_prefix") toolchain_region = self.config.get("toolchain_region") if not self.created: raise seedfarmer.errors.InvalidConfigurationError("The SessionManager object was never properly created...") @@ -212,7 +212,7 @@ def _get_toolchain(self) -> Tuple[Session, "AssumeRoleResponseTypeDef"]: profile_name = self.config.get("profile") project_name = self.config.get("project_name") qualifier = self.config.get("qualifier") if self.config.get("qualifier") else None - role_prefix = self.config.get("role_prefix", "/") + role_prefix = self.config.get("role_prefix") toolchain_region = self.config.get("toolchain_region") _logger.debug( f"""Creating a local session with the following info passed in: diff --git a/seedfarmer/utils.py b/seedfarmer/utils.py index e1461690..5d863b2b 100644 --- a/seedfarmer/utils.py +++ b/seedfarmer/utils.py @@ -148,8 +148,9 @@ def get_toolchain_role_arn( toolchain_account_id: str, project_name: str, qualifier: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, ) -> str: + role_prefix = role_prefix if role_prefix else "/" return ( f"arn:{partition}:iam::{toolchain_account_id}:role{role_prefix}" f"{get_toolchain_role_name(project_name, qualifier)}" @@ -166,8 +167,9 @@ def get_deployment_role_arn( deployment_account_id: str, project_name: str, qualifier: Optional[str] = None, - role_prefix: str = "/", + role_prefix: Optional[str] = None, ) -> str: + role_prefix = role_prefix if role_prefix else "/" return ( f"arn:{partition}:iam::{deployment_account_id}:role{role_prefix}" f"{get_deployment_role_name(project_name, qualifier)}"