Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
JonathanLoscalzo committed May 16, 2023
1 parent 1e6a4d3 commit b01ba37
Show file tree
Hide file tree
Showing 13 changed files with 1,607 additions and 35 deletions.
3 changes: 3 additions & 0 deletions iambic/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ def evaluate_on_provider(
"""
from iambic.core.models import AccessModelMixin

if getattr(resource, "organization_account_needed", None):
return getattr(provider_details, "organization_account", False)

no_op_values = [IambicManaged.DISABLED]
if exclude_import_only:
no_op_values.append(IambicManaged.IMPORT_ONLY)
Expand Down
2 changes: 1 addition & 1 deletion iambic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ def config_discovery(repo_dir: str):
)
def import_(repo_dir: str):
config_path = asyncio.run(resolve_config_template_path(repo_dir))
config = asyncio.run(load_config(config_path))
config: Config = asyncio.run(load_config(config_path))
check_and_update_resource_limit(config)
exe_message = ExecutionMessage(
execution_id=str(uuid.uuid4()), command=Command.IMPORT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,19 @@ Resources:
- sqs:GetQueueAttributes
Resource:
- 'arn:aws:sqs:*:*:IAMbicChangeDetectionQueue'
- Sid: SCPsReadWrite
Effect: Allow
Action:
- organizations:CreatePolicy
- organizations:DeletePolicy
- organizations:DescribePolicy
- organizations:UpdatePolicy
- organizations:ListPolicies
- organizations:AttachPolicy
- organizations:DetachPolicy
- organizations:TagResource
- organizations:UntagResource
- organizations:ListTagsForResource
Resource: arn:aws:organizations::*:policy/o-*/*
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/ReadOnlyAccess'
6 changes: 6 additions & 0 deletions iambic/plugins/v0_1_0/aws/event_bridge/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class ManagedPolicyMessageDetails(PydanticBaseModel):
delete: bool


class SCPPolicyMessageDetails(PydanticBaseModel):
account_id: str
policy_name: str
delete: bool


class RoleMessageDetails(PydanticBaseModel):
account_id: str
role_name: str
Expand Down
133 changes: 101 additions & 32 deletions iambic/plugins/v0_1_0/aws/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@
generate_permission_set_map,
)
from iambic.plugins.v0_1_0.aws.models import AWSAccount
from iambic.plugins.v0_1_0.aws.organizations.scp.models import AWS_SCP_POLICY_TEMPLATE
from iambic.plugins.v0_1_0.aws.organizations.scp.template_generation import (
collect_aws_scp_policies,
generate_aws_scp_policy_templates,
get_organizations_account_map,
)
from iambic.plugins.v0_1_0.aws.organizations.scp.utils import (
service_control_policy_is_enabled,
)

if TYPE_CHECKING:
from iambic.plugins.v0_1_0.aws.iambic_plugin import AWSConfig
Expand All @@ -78,7 +87,7 @@ async def load(config: AWSConfig) -> AWSConfig:
orgs_accounts = await asyncio.gather(
*[org.get_accounts() for org in config.organizations]
)
for org_accounts in orgs_accounts:
for org_accounts, org in zip(orgs_accounts, config.organizations):
for account in org_accounts:
if (
account_elem := config_account_idx_map.get(account.account_id)
Expand All @@ -89,6 +98,15 @@ async def load(config: AWSConfig) -> AWSConfig:
config.accounts[
account_elem
].identity_center_details = account.identity_center_details

# if the account is an organization account, set the organization details
if org.org_account_id == account.account_id:
await config.accounts[
account_elem
].set_account_organization_details(
organization=org,
config=config,
)
else:
log.warning(
"Account not found in config. Account will be ignored.",
Expand Down Expand Up @@ -396,41 +414,92 @@ async def import_aws_resources(
)
)

if not exe_message.metadata or exe_message.metadata["service"] == "iam":
iam_template_map = None
tasks += await import_organization_resources(
exe_message, config, base_output_dir, messages, remote_worker
)

if not remote_worker or exe_message.metadata:
iam_template_map = await get_existing_template_map(
repo_dir=base_output_dir,
template_type="AWS::IAM.*",
nested=True,
)
# if not exe_message.metadata or exe_message.metadata["service"] == "iam":
# iam_template_map = None

# if not remote_worker or exe_message.metadata:
# iam_template_map = await get_existing_template_map(
# repo_dir=base_output_dir,
# template_type="AWS::IAM.*",
# nested=True,
# )
# config.accounts = config.accounts[0:2]
# tasks.append(
# import_service_resources(
# exe_message,
# config,
# base_output_dir,
# "iam",
# [
# # collect_aws_roles,
# # collect_aws_users,
# # collect_aws_groups,
# # collect_aws_managed_policies,
# ],
# [
# # generate_aws_role_templates,
# # generate_aws_user_templates,
# # generate_aws_group_templates,
# # generate_aws_managed_policy_templates,
# ],
# messages,
# remote_worker,
# iam_template_map,
# )
# )

tasks.append(
import_service_resources(
exe_message,
config,
base_output_dir,
"iam",
[
collect_aws_roles,
collect_aws_groups,
collect_aws_users,
collect_aws_managed_policies,
],
[
generate_aws_role_templates,
generate_aws_user_templates,
generate_aws_group_templates,
generate_aws_managed_policy_templates,
],
messages,
remote_worker,
iam_template_map,
)
await asyncio.gather(*tasks)


async def import_organization_resources(
exe_message: ExecutionMessage,
config: AWSConfig,
base_output_dir: str,
messages: list = None,
remote_worker=None,
) -> list:
tasks = []
if len(config.organizations) > 0:
exe_messages = await config.get_command_by_organization_account(exe_message)
scp_template_map = await get_existing_template_map(
repo_dir=base_output_dir,
template_type=AWS_SCP_POLICY_TEMPLATE,
nested=True,
)

await asyncio.gather(*tasks)
for exe_msg in exe_messages:
aws_account_map: dict[
str, AWSAccount
] = await get_organizations_account_map(exe_msg, config)
aws_account = aws_account_map[exe_msg.provider_id] # type: ignore
org_client = await aws_account.get_boto3_client("organizations")

if not (await service_control_policy_is_enabled(org_client)):
pass

# this is also configured at aws config load method
await aws_account.set_account_organization_details(
await config.get_organization_from_account(exe_msg.provider_id), config
)
tasks.append(
import_service_resources(
exe_msg,
config,
base_output_dir,
"scp",
[collect_aws_scp_policies],
[generate_aws_scp_policy_templates],
messages,
remote_worker,
scp_template_map,
)
)

return tasks


async def detect_changes( # noqa: C901
Expand Down
30 changes: 30 additions & 0 deletions iambic/plugins/v0_1_0/aws/iambic_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic import BaseModel, Field, validator

from iambic.core.iambic_plugin import ProviderPlugin
from iambic.core.models import ExecutionMessage
from iambic.plugins.v0_1_0 import PLUGIN_VERSION
from iambic.plugins.v0_1_0.aws.handlers import (
apply,
Expand All @@ -23,6 +24,7 @@
AwsIdentityCenterPermissionSetTemplate,
)
from iambic.plugins.v0_1_0.aws.models import AWSAccount, AWSOrganization
from iambic.plugins.v0_1_0.aws.organizations.scp.models import AwsScpPolicyTemplate


class AWSConfig(BaseModel):
Expand Down Expand Up @@ -105,6 +107,33 @@ async def get_boto_session_from_arn(self, arn: str, region_name: str = None):
aws_account = aws_account_map[account_id]
return await aws_account.get_boto3_session(region_name)

async def get_organization_accounts(self) -> list[AWSAccount]:
org_accounts = []
# each organization has an account, retrieve it
for org in self.organizations:
org_accounts += filter(
lambda acc: acc.account_id == org.org_account_id, self.accounts
)

return org_accounts

async def get_organization_from_account(self, account_id) -> AWSOrganization:
"""Get the organization that owns the account"""
return list(
filter(lambda org: account_id == org.org_account_id, self.organizations)
)[0]

async def get_command_by_organization_account(
self, command: ExecutionMessage
) -> list[ExecutionMessage]:
commands = []
for org in self.organizations:
command_cp = command.copy()
command_cp.provider_id = org.org_account_id
commands.append(command_cp)

return commands


IAMBIC_PLUGIN = ProviderPlugin(
config_name="aws",
Expand All @@ -122,5 +151,6 @@ async def get_boto_session_from_arn(self, arn: str, region_name: str = None):
AwsIamRoleTemplate,
AwsIamUserTemplate,
AwsIamManagedPolicyTemplate,
AwsScpPolicyTemplate,
],
)
22 changes: 21 additions & 1 deletion iambic/plugins/v0_1_0/aws/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def resource_id(self):

class BaseAWSAccountAndOrgModel(PydanticBaseModel):
default_region: Optional[RegionName] = Field(
RegionName.us_east_1,
RegionName.us_west_2,
description="Default region to use when making AWS requests",
)
aws_profile: Optional[str] = Field(
Expand Down Expand Up @@ -298,6 +298,11 @@ class AWSAccount(ProviderChild, BaseAWSAccountAndOrgModel):
description="The role arn to assume into when making calls to the account",
exclude=True,
)
organization_account: bool = Field(
False, description="if this is an organization account"
)
organization: Optional[AWSOrganization] = Field(None)
aws_config: Optional[AWSConfig] = Field(None)

class Config:
fields = {"hub_session_info": {"exclude": True}}
Expand Down Expand Up @@ -445,6 +450,18 @@ async def set_identity_center_details(
group["GroupId"]: group for group in user_or_group["Groups"]
}

async def set_account_organization_details(
self,
organization: AWSOrganization,
config: AWSConfig,
):
"""Set the account's organization details
when the account is the organization account
"""
self.organization_account = True
self.organization = organization
self.aws_config = config

def dict(
self,
*,
Expand All @@ -464,6 +481,9 @@ def dict(
"boto3_session_map",
"hub_session_info",
"identity_center_details",
"organization_account",
"organization",
"aws_config",
}
if exclude:
exclude.update(required_exclude)
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions iambic/plugins/v0_1_0/aws/organizations/scp/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class OrganizationAccountRequiredException(Exception):
pass
Loading

0 comments on commit b01ba37

Please sign in to comment.