Skip to content

Commit 4e8fc4b

Browse files
pc-alvesjmcs
authored andcommitted
Add Cross Stack Policy to IAM Role (#562)
1 parent 7943fff commit 4e8fc4b

File tree

9 files changed

+467
-17
lines changed

9 files changed

+467
-17
lines changed

senza/components/elastic_load_balancer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def get_ssl_cert(subdomain, main_zone, ssl_cert, account_info: AccountArguments)
6262
# the priority is acm_certificate first and iam_certificate second
6363
certificates = (
6464
acm_certificates + iam_certificates
65-
) # type: List[Union[ACMCertificate, IAMServerCertificate]]
65+
) # type: List[Union[ACMCertificate, IAMServerCertificate]] # noqa: F821
6666
try:
6767
certificate = certificates[0]
6868
ssl_cert = certificate.arn

senza/components/elastigroup.py

+85-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
from senza.components.auto_scaling_group import normalize_network_threshold
1414
from senza.components.taupage_auto_scaling_group import check_application_id, check_application_version, \
1515
check_docker_image_exists, generate_user_data
16-
from senza.utils import ensure_keys
16+
from senza.utils import ensure_keys, CROSS_STACK_POLICY_NAME
1717
from senza.spotinst import MissingSpotinstAccount
18+
import senza.manaus.iam
1819

1920
ELASTIGROUP_RESOURCE_TYPE = 'Custom::elastigroup'
2021
SPOTINST_LAMBDA_FORMATION_ARN = 'arn:aws:lambda:{}:178579023202:function:spotinst-cloudformation'
@@ -28,6 +29,88 @@
2829
ELASTIGROUP_DEFAULT_PRODUCT = "Linux/UNIX"
2930

3031

32+
def get_instance_profile_from_definition(definition, elastigroup_config):
33+
launch_spec = elastigroup_config["compute"]["launchSpecification"]
34+
35+
if "iamRole" not in launch_spec:
36+
return None
37+
38+
if "name" in launch_spec["iamRole"]:
39+
if isinstance(launch_spec["iamRole"]["name"], dict):
40+
instance_profile_id = launch_spec["iamRole"]["name"]["Ref"]
41+
instance_profile = definition["Resources"].get(instance_profile_id, None)
42+
if instance_profile is None:
43+
raise click.UsageError("Instance Profile referenced is not present in Resources")
44+
45+
if instance_profile["Type"] != "AWS::IAM::InstanceProfile":
46+
raise click.UsageError(
47+
"Instance Profile references a Resource that is not of type 'AWS::IAM::InstanceProfile'")
48+
49+
return instance_profile
50+
51+
return None
52+
53+
54+
def get_instance_profile_role(instance_profile, definition):
55+
roles = instance_profile["Properties"]["Roles"]
56+
if isinstance(roles[0], dict):
57+
role_id = roles[0]["Ref"]
58+
role = definition["Resources"].get(role_id, None)
59+
if role is None:
60+
raise click.UsageError("Instance Profile references a Role that is not present in Resources")
61+
62+
if role["Type"] != "AWS::IAM::Role":
63+
raise click.UsageError("Instance Profile Role references a Resource that is not of type 'AWS::IAM::Role'")
64+
65+
return role
66+
67+
return None
68+
69+
70+
def create_cross_stack_policy_document():
71+
return {
72+
"Version": "2012-10-17",
73+
"Statement": [
74+
{
75+
"Effect": "Allow",
76+
"Action": [
77+
"cloudformation:SignalResource",
78+
"cloudformation:DescribeStackResource"
79+
],
80+
"Resource": "*"
81+
}
82+
]
83+
}
84+
85+
86+
def find_or_create_cross_stack_policy():
87+
return senza.manaus.iam.find_or_create_policy(policy_name=CROSS_STACK_POLICY_NAME,
88+
policy_document=create_cross_stack_policy_document(),
89+
description="Required permissions for EC2 instances created by "
90+
"Spotinst to signal CloudFormation")
91+
92+
93+
def patch_cross_stack_policy(definition, elastigroup_config):
94+
"""
95+
This function will make sure that the role used in the Instance Profile includes the Cross Stack API
96+
requests policy, needed for Elastigroups to run as expected.
97+
"""
98+
instance_profile = get_instance_profile_from_definition(definition, elastigroup_config)
99+
if instance_profile is None:
100+
return
101+
102+
instance_profile_role = get_instance_profile_role(instance_profile, definition)
103+
if instance_profile_role is None:
104+
return
105+
106+
cross_stack_policy = find_or_create_cross_stack_policy()
107+
108+
role_properties = instance_profile_role["Properties"]
109+
managed_policies_set = set(role_properties.get("ManagedPolicyArns", []))
110+
managed_policies_set.add(cross_stack_policy["Arn"])
111+
role_properties["ManagedPolicyArns"] = list(managed_policies_set)
112+
113+
31114
def component_elastigroup(definition, configuration, args, info, force, account_info):
32115
"""
33116
This component creates a Spotinst Elastigroup CloudFormation custom resource template.
@@ -62,6 +145,7 @@ def component_elastigroup(definition, configuration, args, info, force, account_
62145
extract_auto_scaling_rules(configuration, elastigroup_config)
63146
extract_block_mappings(configuration, elastigroup_config)
64147
extract_instance_profile(args, definition, configuration, elastigroup_config)
148+
patch_cross_stack_policy(definition, elastigroup_config)
65149
# cfn definition
66150
access_token = _extract_spotinst_access_token(definition)
67151
config_name = configuration["Name"]

senza/manaus/iam.py

+40
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import Any, Dict, Iterator, Optional, Union
1515

1616
import boto3
17+
import json
1718
from botocore.exceptions import ClientError
1819

1920
from .boto_proxy import BotoClientProxy
@@ -161,3 +162,42 @@ def get_certificates(
161162
continue
162163

163164
yield certificate
165+
166+
167+
def _get_policy_by_name(policy_name, iam_client):
168+
"""
169+
This function goes through all the policies in the AWS account and return the first one matching the policy_name
170+
input parameter
171+
"""
172+
paginator = iam_client.get_paginator("list_policies")
173+
174+
page_iterator = paginator.paginate()
175+
176+
for page in page_iterator:
177+
if "Policies" in page:
178+
for policy in page["Policies"]:
179+
if policy["PolicyName"] == policy_name:
180+
return policy
181+
182+
return None
183+
184+
185+
def find_or_create_policy(policy_name, policy_document, description):
186+
"""
187+
This function will look for a policy name with `policy_name`.
188+
If not found, it will create the policy using the provided `policy_name` and `policy_document`.
189+
190+
:return: Policy object
191+
"""
192+
iam_client = boto3.client("iam")
193+
194+
policy = _get_policy_by_name(policy_name, iam_client)
195+
if policy is None:
196+
response = iam_client.create_policy(
197+
PolicyName=policy_name,
198+
PolicyDocument=json.dumps(policy_document),
199+
Description=description
200+
)
201+
policy = response["Policy"]
202+
203+
return policy

senza/templates/_helper.py

+69-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from click import confirm
99
from clickclick import Action
1010
from senza.aws import get_account_alias, get_account_id, get_security_group
11+
from senza.utils import CROSS_STACK_POLICY_NAME
12+
import senza.manaus.iam
1113

1214
from ..manaus.boto_proxy import BotoClientProxy
1315

@@ -130,7 +132,7 @@ def get_mint_bucket_name(region: str):
130132
return bucket_name
131133

132134

133-
def get_iam_role_policy(application_id: str, bucket_name: str, region: str):
135+
def create_mint_read_policy_document(application_id: str, bucket_name: str, region: str):
134136
return {
135137
"Version": "2012-10-17",
136138
"Statement": [
@@ -146,6 +148,33 @@ def get_iam_role_policy(application_id: str, bucket_name: str, region: str):
146148
}
147149

148150

151+
def create_cross_stack_policy_document():
152+
return {
153+
"Version": "2012-10-17",
154+
"Statement": [
155+
{
156+
"Effect": "Allow",
157+
"Action": [
158+
"cloudformation:SignalResource",
159+
"cloudformation:DescribeStackResource"
160+
],
161+
"Resource": "*"
162+
}
163+
]
164+
}
165+
166+
167+
def check_cross_stack_policy(iam, role_name: str):
168+
try:
169+
iam.get_role_policy(
170+
RoleName=role_name,
171+
PolicyName=CROSS_STACK_POLICY_NAME
172+
)
173+
return True
174+
except botocore.exceptions.ClientError:
175+
return False
176+
177+
149178
def check_iam_role(application_id: str, bucket_name: str, region: str):
150179
role_name = "app-{}".format(application_id)
151180
with Action("Checking IAM role {}..".format(role_name)):
@@ -167,6 +196,8 @@ def check_iam_role(application_id: str, bucket_name: str, region: str):
167196
],
168197
"Version": "2008-10-17",
169198
}
199+
200+
create = False
170201
if not exists:
171202
create = confirm(
172203
"IAM role {} does not exist. "
@@ -180,20 +211,49 @@ def check_iam_role(application_id: str, bucket_name: str, region: str):
180211
AssumeRolePolicyDocument=json.dumps(assume_role_policy_document),
181212
)
182213

183-
update_policy = bucket_name is not None and (
184-
not exists
185-
or confirm(
186-
"IAM role {} already exists. ".format(role_name)
187-
+ "Do you want Senza to overwrite the role policy?"
214+
attach_mint_read_policy = bucket_name is not None and (
215+
(not exists and create)
216+
or (
217+
exists and confirm(
218+
"IAM role {} already exists. ".format(role_name)
219+
+ "Do you want Senza to overwrite the role policy?"
220+
)
188221
)
189222
)
190-
if update_policy:
223+
if attach_mint_read_policy:
191224
with Action("Updating IAM role policy of {}..".format(role_name)):
192-
policy = get_iam_role_policy(application_id, bucket_name, region)
225+
mint_read_policy = create_mint_read_policy_document(application_id, bucket_name, region)
193226
iam.put_role_policy(
194227
RoleName=role_name,
195228
PolicyName=role_name,
196-
PolicyDocument=json.dumps(policy),
229+
PolicyDocument=json.dumps(mint_read_policy),
230+
)
231+
232+
attach_cross_stack_policy(exists, create, role_name, iam)
233+
234+
235+
def find_or_create_cross_stack_policy():
236+
return senza.manaus.iam.find_or_create_policy(policy_name=CROSS_STACK_POLICY_NAME,
237+
policy_document=create_cross_stack_policy_document(),
238+
description="Required permissions for EC2 instances created by "
239+
"Spotinst to signal CloudFormation")
240+
241+
242+
def attach_cross_stack_policy(pre_existing_role, role_created, role_name, iam_client):
243+
if not pre_existing_role and not role_created:
244+
return
245+
246+
cross_stack_policy_exists = False
247+
if pre_existing_role:
248+
cross_stack_policy_exists = check_cross_stack_policy(iam_client, role_name)
249+
250+
if role_created or not cross_stack_policy_exists:
251+
with Action("Updating IAM role policy of {}..".format(role_name)):
252+
policy = find_or_create_cross_stack_policy()
253+
254+
iam_client.attach_role_policy(
255+
RoleName=role_name,
256+
PolicyArn=policy["Arn"],
197257
)
198258

199259

senza/utils.py

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import re
77
import pystache
88

9+
CROSS_STACK_POLICY_NAME = "system-cf-notifications"
10+
911

1012
def named_value(dictionary):
1113
"""

0 commit comments

Comments
 (0)