Skip to content

Commit

Permalink
Cross-Cccount assume role generally and locally for lookup (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnPreston authored Nov 5, 2020
1 parent c46c208 commit be536c1
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 22 deletions.
6 changes: 6 additions & 0 deletions ecs_composex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ def main_parser():
help="Runs spotfleet for EC2. If used in combination "
"of --use-fargate, it will create an additional SpotFleet",
)
base_command_parser.add_argument(
"--role-arn",
dest=ComposeXSettings.arn_arg,
help="Allow you to run API calls using a specific IAM role, within same or for cross-account",
required=False,
)
for command in ComposeXSettings.active_commands:
cmd_parsers.add_parser(
name=command["name"],
Expand Down
52 changes: 52 additions & 0 deletions ecs_composex/common/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,61 @@
Common functions and variables fetched from AWS.
"""
import re
import boto3
from botocore.exceptions import ClientError

from ecs_composex.common import LOG, keyisset
from ecs_composex.iam import ROLE_ARN_ARG
from ecs_composex.iam import validate_iam_role_arn


def get_cross_role_session(session, arn, session_name=None):
"""
Function to override ComposeXSettings session to specific session for Lookup
:param boto3.session.Session session: The original session fetching the credentials for X-Role
:param str arn:
:param str session_name: Override name of the session
:return: boto3 session from lookup settings
:rtype: boto3.session.Session
"""
if not session_name:
session_name = "ComposeX@Lookup"
validate_iam_role_arn(arn)
try:
if not session:
session = boto3.session.Session()
creds = session.client("sts").assume_role(
RoleArn=arn,
RoleSessionName=session_name,
DurationSeconds=900,
)
LOG.info(
f"Successfully assumed role. Session ID: {creds['AssumedRoleUser']['AssumedRoleId']}"
)
return boto3.session.Session(
aws_access_key_id=creds["Credentials"]["AccessKeyId"],
aws_session_token=creds["Credentials"]["SessionToken"],
aws_secret_access_key=creds["Credentials"]["SecretAccessKey"],
)
except ClientError:
LOG.error(f"Failed to use the Role ARN {arn}")
raise


def define_lookup_role_from_info(info, session):
"""
Function to override ComposeXSettings session to specific session for Lookup
:param info:
:param session:
:return: boto3 session from lookup settings
:rtype: boto3.session.Session
"""
if not keyisset(ROLE_ARN_ARG, info):
return session
validate_iam_role_arn(info[ROLE_ARN_ARG])
return get_cross_role_session(session, info[ROLE_ARN_ARG])


def define_tagsgroups_filter_tags(tags):
Expand Down
25 changes: 22 additions & 3 deletions ecs_composex/common/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
ComposeFamily,
set_service_ports,
)
from ecs_composex.iam import ROLE_ARN_ARG
from ecs_composex.iam import validate_iam_role_arn
from ecs_composex.common.aws import get_cross_role_session


def render_services_ports(services):
Expand Down Expand Up @@ -195,6 +198,7 @@ class ComposeXSettings(object):

region_arg = "RegionName"
zones_arg = "Zones"
arn_arg = ROLE_ARN_ARG

deploy_arg = "up"
render_arg = "render"
Expand Down Expand Up @@ -250,7 +254,7 @@ def __init__(self, content=None, profile_name=None, session=None, **kwargs):
Class to init the configuration
"""
self.session = boto3.session.Session()
self.override_session(session, profile_name)
self.override_session(session, profile_name, kwargs)
self.aws_region = (
kwargs[self.region_arg]
if keyisset(self.region_arg, kwargs)
Expand Down Expand Up @@ -455,17 +459,32 @@ def parse_command(self, kwargs, content=None):
self.init_s3()
exit(0)

def override_session(self, session, profile_name):
def override_session(self, session, profile_name, kwargs):
"""
Method to set the session based on input params
:param boto3.session.Session session: The session to override the API calls with
:param str profile_name: Name of a profile configured in .aws/config
:param dict kwargs: CLI kwargs
"""
if profile_name and not session:
self.session = boto3.session.Session(profile_name=profile_name)
elif session and not profile_name:
elif session and not (profile_name or keyisset(self.arn_arg, kwargs)):
self.session = session
if keyisset(self.arn_arg, kwargs):
validate_iam_role_arn(arn=kwargs[self.arn_arg])
if session:
self.session = get_cross_role_session(
session,
kwargs[ROLE_ARN_ARG],
session_name=f"ComposeXSettings@{kwargs[self.command_arg]}",
)
else:
self.session = get_cross_role_session(
self.session,
kwargs[ROLE_ARN_ARG],
session_name=f"ComposeXSettings@{kwargs[self.command_arg]}",
)

def set_output_settings(self, kwargs):
"""
Expand Down
10 changes: 7 additions & 3 deletions ecs_composex/dynamodb/dynamodb_aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
from botocore.exceptions import ClientError

from ecs_composex.common import keyisset, LOG
from ecs_composex.common.aws import find_aws_resource_arn_from_tags_api
from ecs_composex.common.aws import (
find_aws_resource_arn_from_tags_api,
define_lookup_role_from_info,
)
from ecs_composex.dynamodb.dynamodb_params import TABLE_NAME, TABLE_ARN


Expand Down Expand Up @@ -69,14 +72,15 @@ def lookup_dynamodb_config(lookup, session):
"regexp": r"(?:^arn:aws(?:-[a-z]+)?:dynamodb:[\S]+:[0-9]+:table\/)([\S]+)$"
},
}
lookup_session = define_lookup_role_from_info(lookup, session)
table_arn = find_aws_resource_arn_from_tags_api(
lookup,
session,
lookup_session,
"dynamodb:table",
types=dynamodb_types,
)
if not table_arn:
return None
config = get_table_config(table_arn, session)
config = get_table_config(table_arn, lookup_session)
LOG.debug(config)
return config
18 changes: 18 additions & 0 deletions ecs_composex/iam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@
POLICY_PATTERN = r"((^([a-zA-Z0-9-_.\/]+)$)|(^(arn:aws:iam::(aws|[0-9]{12}):policy\/)[a-zA-Z0-9-_.\/]+$))"
POLICY_RE = re.compile(POLICY_PATTERN)

ROLE_ARN_ARG = "RoleArn"


def validate_iam_role_arn(arn):
"""
Function to validate IAM ROLE ARN format
:param str arn:
:return: resource match
:rtype: re.match
"""
arn_valid = re.compile(r"^arn:aws(?:-[a-z]+)?:iam::[0-9]{12}:role/[\S]+$")
if not arn_valid.match(arn):
raise ValueError(
"The role ARN needs to be a valid ARN of format",
r"^arn:aws(?:-[a-z]+)?:iam::[0-9]{12}:role/[\S]+$",
)
return arn_valid.match(arn)


def service_role_trust_policy(service_name):
"""
Expand Down
10 changes: 7 additions & 3 deletions ecs_composex/kms/kms_aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import re
from botocore.exceptions import ClientError
from ecs_composex.common import LOG, keyisset
from ecs_composex.common.aws import find_aws_resource_arn_from_tags_api
from ecs_composex.common.aws import (
find_aws_resource_arn_from_tags_api,
define_lookup_role_from_info,
)

from ecs_composex.kms.kms_params import (
KMS_KEY_ARN,
Expand Down Expand Up @@ -83,14 +86,15 @@ def lookup_key_config(lookup, session):
"regexp": r"(?:^arn:aws(?:-[a-z]+)?:kms:[\S]+:[0-9]+:)((alias/)([\S]+))$"
},
}
lookup_session = define_lookup_role_from_info(lookup, session)
key_arn = find_aws_resource_arn_from_tags_api(
lookup,
session,
lookup_session,
"kms:key",
types=kms_types,
)
if not key_arn:
return None
config = get_key_config(key_arn, session)
config = get_key_config(key_arn, lookup_session)
LOG.debug(config)
return config
8 changes: 5 additions & 3 deletions ecs_composex/rds/rds_aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ecs_composex.common import keyisset, LOG
from ecs_composex.common.aws import (
find_aws_resource_arn_from_tags_api,
define_lookup_role_from_info,
)


Expand Down Expand Up @@ -171,13 +172,14 @@ def lookup_rds_resource(lookup, session):
res_type = "cluster"
elif keyisset("db", lookup):
res_type = "db"
lookup_session = define_lookup_role_from_info(lookup, session)
db_arn = find_aws_resource_arn_from_tags_api(
lookup[res_type], session, f"rds:{res_type}", types=rds_types
lookup[res_type], lookup_session, f"rds:{res_type}", types=rds_types
)
if not db_arn:
return None
db_config = return_db_config(db_arn, session, res_type)
handle_secret(lookup, db_config, session)
db_config = return_db_config(db_arn, lookup_session, res_type)
handle_secret(lookup, db_config, lookup_session)
patch_db_vs_cluster(db_config, res_type)

LOG.debug(db_config)
Expand Down
10 changes: 7 additions & 3 deletions ecs_composex/s3/s3_aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
from botocore.exceptions import ClientError

from ecs_composex.common import LOG, keyisset
from ecs_composex.common.aws import find_aws_resource_arn_from_tags_api
from ecs_composex.common.aws import (
find_aws_resource_arn_from_tags_api,
define_lookup_role_from_info,
)


def return_bucket_config(bucket_arn, session):
Expand Down Expand Up @@ -76,14 +79,15 @@ def lookup_bucket_config(lookup, session):
s3_types = {
"s3": {"regexp": r"(?:^arn:aws(?:-[a-z]+)?:s3:::)([\S]+)$"},
}
lookup_session = define_lookup_role_from_info(lookup, session)
bucket_arn = find_aws_resource_arn_from_tags_api(
lookup,
session,
lookup_session,
"s3",
types=s3_types,
)
if not bucket_arn:
return None
config = return_bucket_config(bucket_arn, session)
config = return_bucket_config(bucket_arn, lookup_session)
LOG.debug(config)
return config
10 changes: 7 additions & 3 deletions ecs_composex/sns/sns_aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import re
from botocore.exceptions import ClientError
from ecs_composex.common import LOG, keyisset
from ecs_composex.common.aws import find_aws_resource_arn_from_tags_api
from ecs_composex.common.aws import (
find_aws_resource_arn_from_tags_api,
define_lookup_role_from_info,
)

from ecs_composex.sns.sns_params import TOPIC_NAME, TOPIC_ARN, TOPIC_KMS_KEY

Expand Down Expand Up @@ -72,14 +75,15 @@ def lookup_topic_config(lookup, session):
sns_types = {
"sns": {"regexp": r"(?:^arn:aws(?:-[a-z]+)?:sns:[\S]+:[0-9]+:)([\S]+)$"},
}
lookup_session = define_lookup_role_from_info(lookup, session)
topic_arn = find_aws_resource_arn_from_tags_api(
lookup,
session,
lookup_session,
"sns",
types=sns_types,
)
if not topic_arn:
return None
config = get_topic_config(topic_arn, session)
config = get_topic_config(topic_arn, lookup_session)
LOG.debug(config)
return config
10 changes: 7 additions & 3 deletions ecs_composex/sqs/sqs_aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import re
from botocore.exceptions import ClientError
from ecs_composex.common import LOG, keyisset
from ecs_composex.common.aws import find_aws_resource_arn_from_tags_api
from ecs_composex.common.aws import (
find_aws_resource_arn_from_tags_api,
define_lookup_role_from_info,
)

from ecs_composex.sqs.sqs_params import SQS_ARN, SQS_URL, SQS_NAME, SQS_KMS_KEY_T

Expand Down Expand Up @@ -88,14 +91,15 @@ def lookup_queue_config(lookup, session):
sqs_types = {
"sqs": {"regexp": r"(?:^arn:aws(?:-[a-z]+)?:sqs:[\S]+:[0-9]+:)([\S]+)$"},
}
lookup_session = define_lookup_role_from_info(lookup, session)
queue_arn = find_aws_resource_arn_from_tags_api(
lookup,
session,
lookup_session,
"sqs",
types=sqs_types,
)
if not queue_arn:
return None
config = get_queue_config(queue_arn, session)
config = get_queue_config(queue_arn, lookup_session)
LOG.debug(config)
return config
35 changes: 35 additions & 0 deletions pytests/settings/role_arn/sts.AssumeRole_1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"status_code": 200,
"data": {
"Credentials": {
"AccessKeyId": "ASIAVOAWVJQOEVY2AYGS",
"SecretAccessKey": "uqF5Ju6SiZONEREDACTED123p5akjxfOJtmbT/V0ncDrcGwJKEta",
"SessionToken": "FwoGZONEREDACTED123vYXdzEKn//////////wEaDOx+Sc+2WVjMjoMl6CKzAV+99cj7E5U7EGjZuLX+kebhSKEA+9cytjwyWAz593lUJWXoF3u7xA5gBUtksMxS/rxYfOU6uvB71vOz5CjfjUNxnpBk+V9yvPvQCRjhDy8+iWa97U123gyOFLXFD0Hs7IUBBAUpUwfXy3FxDhWLIjoHxfp7lVwu7uTV1AXGhR6A3+uHxGiBT/4OAqrw24DjAYlAqlZONEREDACTED123lZvB38g7d7ZgTfsA2lEGpxz2vBOiwkBfMVm6qizfKLK8kP0FMi072TPEoLu5bIj5AgHVQOnhaDhoucxIAP3linG2SrqlEcRQAwPkHQTp4uFIDgd=",
"Expiration": {
"__class__": "datetime",
"year": 2020,
"month": 11,
"day": 5,
"hour": 16,
"minute": 0,
"second": 54,
"microsecond": 0
}
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAVOAWVJQOLB24ILL25:ComposeX@render",
"Arn": "arn:aws:sts::000000000000:assumed-role/testx/ComposeX@render"
},
"ResponseMetadata": {
"RequestId": "e6ed5816-c773-4e7c-a9f5-567fe35334ad",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"x-amzn-requestid": "e6ed5816-c773-4e7c-a9f5-567fe35334ad",
"content-type": "text/xml",
"content-length": "1062",
"date": "Thu, 05 Nov 2020 15:45:54 GMT"
},
"RetryAttempts": 0
}
}
}
21 changes: 21 additions & 0 deletions pytests/settings/role_arn/sts.AssumeRole_2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"status_code": 403,
"data": {
"Error": {
"Type": "Sender",
"Code": "AccessDenied",
"Message": "User: arn:aws:sts::000000000000:assumed-role/AWSReservedSSO_PowerUserAccess_e9ddee7954f3b1a9/[email protected] is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000000:role/test"
},
"ResponseMetadata": {
"RequestId": "3232e2a4-3bba-4068-905a-c9eed6aa3e4f",
"HTTPStatusCode": 403,
"HTTPHeaders": {
"x-amzn-requestid": "3232e2a4-3bba-4068-905a-c9eed6aa3e4f",
"content-type": "text/xml",
"content-length": "451",
"date": "Thu, 05 Nov 2020 15:47:19 GMT"
},
"RetryAttempts": 0
}
}
}
Loading

0 comments on commit be536c1

Please sign in to comment.