Skip to content

Commit

Permalink
[Resolve #1080] Added duration_seconds parameter to adopt DurationSec…
Browse files Browse the repository at this point in the history
…onds in boto (#1210)

Added a new parameter iam_role_session_duration in at stack level so users can edit the session duration when connection_manager assumes iam_role that is provided.

This new added parameter is an optional parameter. If not provided in the stack yamls, connection_manager will use default value of 3600s to assume iam_role. (same as before.)

Added type annotation for stacks class to ensure iam_role_session_duration is int type.

Also updated the documentation of how to use iam_role_session_duration.
  • Loading branch information
shixuyue committed Mar 16, 2022
1 parent 3fcb295 commit b2ba807
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 18 deletions.
19 changes: 19 additions & 0 deletions docs/_source/docs/stack_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ particular Stack. The available keys are listed below.
- `protected`_ *(optional)*
- `role_arn`_ *(optional)*
- `iam_role`_ *(optional)*
- `iam_role_session_duration`_ *(optional)*
- `sceptre_user_data`_ *(optional)*
- `stack_name`_ *(optional)*
- `stack_tags`_ *(optional)*
Expand Down Expand Up @@ -243,6 +244,24 @@ stack. The ``iam_role`` configuration does not configure anything on the stack i
For more information on this configuration, its implications, and its uses, see
:ref:`Sceptre and IAM: iam_role <iam_role_permissions>`.

iam_role_session_duration
~~~~~~~~
* Resolvable: No
* Can be inherited from StackGroup: Yes
* Inheritance strategy: Overrides parent if set

This is the session duration when **Sceptre** *assumes* the **iam_role** IAM Role using AWS STS when
executing any actions on the Stack.

.. warning::

If you set the value of ``iam_role_session_duration`` to a number that *GREATER* than 3600, you
will need to make sure that the ``iam_role`` has a configuration of ``MaxSessionDuration``, and
its value is *GREATER* than or equal to the value of ``iam_role_session_duration``.

For more information on this configuration, its implications, and its uses, see
:ref:`Sceptre and IAM: iam_role_session_duration <iam_role_permissions>`.

sceptre_user_data
~~~~~~~~~~~~~~~~~
* Resolvable: Yes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ notifications:
- !stack_output project-deps/dependencies/topic.yaml::TopicArn

iam_role: !stack_output project-deps/dependencies/assumed-role.yaml::RoleArn
iam_role_session_duration: 1800
stack_tags:
greeting: !rcmd "echo 'hello' | tr -d '\n'"
nonexistant: !no_value
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Resources:
- Action: "iam:PassRole"
Effect: Allow
Resource: "*"
MaxSessionDuration: 43200

Outputs:
RoleArn:
Expand Down
3 changes: 3 additions & 0 deletions sceptre/config/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": strategies.list_join,
"hooks": strategies.child_wins,
"iam_role": strategies.child_wins,
"iam_role_session_duration": strategies.child_wins,
"notifications": strategies.child_wins,
"on_failure": strategies.child_wins,
"parameters": strategies.child_wins,
Expand Down Expand Up @@ -82,6 +83,7 @@
"dependencies",
"hooks",
"iam_role",
"iam_role_session_duration",
"notifications",
"on_failure",
"parameters",
Expand Down Expand Up @@ -537,6 +539,7 @@ def _construct_stack(self, rel_path, stack_group_config=None):
template_key_prefix=config.get("template_key_prefix"),
required_version=config.get("required_version"),
iam_role=config.get("iam_role"),
iam_role_session_duration=config.get("iam_role_session_duration"),
profile=config.get("profile"),
parameters=config.get("parameters", {}),
sceptre_user_data=config.get("sceptre_user_data", {}),
Expand Down
22 changes: 15 additions & 7 deletions sceptre/connection_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,27 @@ class ConnectionManager(object):
_clients = {}
_stack_keys = {}

def __init__(self, region, profile=None, stack_name=None, iam_role=None):
def __init__(
self, region, profile=None, stack_name=None,
iam_role=None, iam_role_session_duration=None
):

self.logger = logging.getLogger(__name__)

self.region = region
self.profile = profile
self.stack_name = stack_name
self.iam_role = iam_role
self.iam_role_session_duration = iam_role_session_duration

if stack_name:
self._stack_keys[stack_name] = (region, profile, iam_role)

def __repr__(self):
return (
"sceptre.connection_manager.ConnectionManager(region='{0}', "
"profile='{1}', stack_name='{2}', iam_role='{3}')".format(
self.region, self.profile, self.stack_name, self.iam_role
"profile='{1}', stack_name='{2}', iam_role='{3}', iam_role_session_duration='{4}')".format(
self.region, self.profile, self.stack_name, self.iam_role, self.iam_role_session_duration
)
)

Expand Down Expand Up @@ -155,10 +160,13 @@ def _get_session(self, profile, region, iam_role):
sts_client = session.client("sts")
# maximum session name length is 64 chars. 56 + "-session" = 64
session_name = f'{iam_role.split("/")[-1][:56]}-session'
sts_response = sts_client.assume_role(
RoleArn=iam_role,
RoleSessionName=session_name
)
assume_role_kwargs = {
'RoleArn': iam_role,
'RoleSessionName': session_name,
}
if self.iam_role_session_duration:
assume_role_kwargs['DurationSeconds'] = self.iam_role_session_duration
sts_response = sts_client.assume_role(**assume_role_kwargs)

credentials = sts_response["Credentials"]
session = boto3.session.Session(
Expand Down
3 changes: 2 additions & 1 deletion sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def __init__(self, stack):
self.logger = logging.getLogger(__name__)
self.connection_manager = ConnectionManager(
self.stack.region, self.stack.profile,
self.stack.external_name, self.stack.iam_role
self.stack.external_name, self.stack.iam_role,
self.stack.iam_role_session_duration
)

@add_stack_hooks
Expand Down
24 changes: 16 additions & 8 deletions sceptre/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sceptre.connection_manager import ConnectionManager
from sceptre.exceptions import InvalidConfigFileError
from sceptre.helpers import get_external_stack_name, sceptreise_path
from sceptre.hooks import HookProperty
from sceptre.hooks import Hook, HookProperty
from sceptre.resolvers import (
ResolvableContainerProperty,
ResolvableValueProperty,
Expand Down Expand Up @@ -116,6 +116,10 @@ class Stack(object):
:param stack_group_config: The StackGroup config for the Stack
:type stack_group_config: dict
:param iam_role_session_duration: The session duration when Scetre assumes a role.\
If not supplied, Sceptre uses default value (3600 seconds)
:type iam_role_session_duration: int
"""
parameters = ResolvableContainerProperty("parameters")
sceptre_user_data = ResolvableContainerProperty(
Expand Down Expand Up @@ -153,12 +157,12 @@ class Stack(object):
hooks = HookProperty("hooks")

def __init__(
self, name, project_code, region, template_path=None, template_handler_config=None,
template_bucket_name=None, template_key_prefix=None, required_version=None,
parameters=None, sceptre_user_data=None, hooks=None, s3_details=None,
iam_role=None, dependencies=None, role_arn=None, protected=False, tags=None,
external_name=None, notifications=None, on_failure=None, profile=None,
stack_timeout=0, stack_group_config={}
self, name: str, project_code: str, region: str, template_path: str = None,
template_handler_config: dict = None, template_bucket_name: str = None, template_key_prefix: str = None,
required_version: str = None, parameters: dict = None, sceptre_user_data: dict = None, hooks: Hook = None,
s3_details: dict = None, iam_role: str = None, dependencies=None, role_arn: str = None, protected: bool = False,
tags: dict = None, external_name: str = None, notifications=None, on_failure: str = None, profile: str = None,
stack_timeout: int = 0, iam_role_session_duration: int = 0, stack_group_config: dict = {}
):
self.logger = logging.getLogger(__name__)

Expand All @@ -181,6 +185,7 @@ def __init__(
self.stack_timeout = stack_timeout
self.profile = profile
self.template_key_prefix = template_key_prefix
self.iam_role_session_duration = iam_role_session_duration

self._template = None
self._connection_manager = None
Expand Down Expand Up @@ -212,6 +217,7 @@ def __repr__(self):
"template_key_prefix={template_key_prefix}, "
"required_version={required_version}, "
"iam_role={iam_role}, "
"iam_role_session_duration={iam_role_session_duration}, "
"profile={profile}, "
"sceptre_user_data={sceptre_user_data}, "
"parameters={parameters}, "
Expand All @@ -236,6 +242,7 @@ def __repr__(self):
template_key_prefix=self.template_key_prefix,
required_version=self.required_version,
iam_role=self.iam_role,
iam_role_session_duration=self.iam_role_session_duration,
profile=self.profile,
sceptre_user_data=self.sceptre_user_data,
parameters=self.parameters,
Expand Down Expand Up @@ -267,6 +274,7 @@ def __eq__(self, stack):
self.template_key_prefix == stack.template_key_prefix and
self.required_version == stack.required_version and
self.iam_role == stack.iam_role and
self.iam_role_session_duration == stack.iam_role_session_duration and
self.profile == stack.profile and
self.sceptre_user_data == stack.sceptre_user_data and
self.parameters == stack.parameters and
Expand Down Expand Up @@ -312,7 +320,7 @@ def connection_manager(self) -> ConnectionManager:
cache_connection_manager = False

connection_manager = ConnectionManager(
self.region, self.profile, self.external_name, iam_role
self.region, self.profile, self.external_name, iam_role, self.iam_role_session_duration
)
if cache_connection_manager:
self._connection_manager = connection_manager
Expand Down
1 change: 1 addition & 0 deletions tests/test_config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def test_construct_stacks_constructs_stack(
s3_details=sentinel.s3_details,
dependencies=["child/level", "top/level"],
iam_role=None,
iam_role_session_duration=None,
role_arn=None,
protected=False,
tags={},
Expand Down
36 changes: 34 additions & 2 deletions tests/test_connection_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def setup_method(self, test_method):
self.stack_name = None
self.profile = None
self.iam_role = None
self.iam_role_session_duration = 3600
self.region = "eu-west-1"

ConnectionManager._boto_sessions = {}
Expand Down Expand Up @@ -50,12 +51,14 @@ def test_connection_manager_initialised_with_all_parameters(self):
region=self.region,
stack_name="stack",
profile="profile",
iam_role="iam_role"
iam_role="iam_role",
iam_role_session_duration=21600
)

assert connection_manager.stack_name == "stack"
assert connection_manager.profile == "profile"
assert connection_manager.iam_role == "iam_role"
assert connection_manager.iam_role_session_duration == 21600
assert connection_manager.region == self.region
assert connection_manager._boto_sessions == {}
assert connection_manager._clients == {}
Expand All @@ -71,7 +74,18 @@ def test_repr(self):
response = self.connection_manager.__repr__()
assert response == "sceptre.connection_manager.ConnectionManager(" \
"region='region', profile='profile', stack_name='stack', "\
"iam_role='iam_role')"
"iam_role='iam_role', iam_role_session_duration='None')"

def test_repr_with_iam_role_session_duration(self):
self.connection_manager.stack_name = "stack"
self.connection_manager.profile = "profile"
self.connection_manager.region = "region"
self.connection_manager.iam_role = "iam_role"
self.connection_manager.iam_role_session_duration = 21600
response = self.connection_manager.__repr__()
assert response == "sceptre.connection_manager.ConnectionManager(" \
"region='region', profile='profile', stack_name='stack', "\
"iam_role='iam_role', iam_role_session_duration='21600')"

def test_boto_session_with_cache(self):
self.connection_manager._boto_sessions["test"] = sentinel.boto_session
Expand Down Expand Up @@ -173,6 +187,24 @@ def test_boto_session_with_iam_role(self, mock_Session):
aws_session_token=credentials["SessionToken"]
)

@patch("sceptre.connection_manager.boto3.session.Session")
def test_boto_session_with_iam_role_session_duration(self, mock_Session):
self.connection_manager._boto_sessions = {}
self.connection_manager.iam_role = "iam_role"
self.connection_manager.iam_role_session_duration = 21600

boto_session = self.connection_manager._get_session(
self.profile, self.region, self.connection_manager.iam_role
)

boto_session.client().assume_role.assert_called_once_with(
RoleArn=self.connection_manager.iam_role,
RoleSessionName="{0}-session".format(
self.connection_manager.iam_role.split("/")[-1]
),
DurationSeconds=21600
)

@patch("sceptre.connection_manager.boto3.session.Session")
def test_boto_session_with_iam_role_returning_empty_credentials(self, mock_Session):
self.connection_manager._boto_sessions = {}
Expand Down
2 changes: 2 additions & 0 deletions tests/test_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def setup_method(self, test_method):
tags={"tag1": "val1"}, external_name=sentinel.external_name,
notifications=[sentinel.notification],
on_failure=sentinel.on_failure, iam_role=sentinel.iam_role,
iam_role_session_duration=sentinel.iam_role_session_duration,
stack_timeout=sentinel.stack_timeout,
stack_group_config={}
)
Expand Down Expand Up @@ -143,6 +144,7 @@ def test_stack_repr(self):
"template_key_prefix=sentinel.template_key_prefix, "\
"required_version=sentinel.required_version, "\
"iam_role=sentinel.iam_role, "\
"iam_role_session_duration=sentinel.iam_role_session_duration, "\
"profile=sentinel.profile, " \
"sceptre_user_data=sentinel.sceptre_user_data, " \
"parameters={'key1': 'val1'}, "\
Expand Down

0 comments on commit b2ba807

Please sign in to comment.