Skip to content

Commit 153e06b

Browse files
authored
feat: Add Step Function Resource (#1601)
Co-authored-by: Jacob Fuss <[email protected]>
1 parent 0aa78af commit 153e06b

File tree

141 files changed

+19500
-137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

141 files changed

+19500
-137
lines changed

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.23.0"
1+
__version__ = "1.24.0"

samtranslator/model/iam.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ class IAMRole(Resource):
1818

1919

2020
class IAMRolePolicies:
21+
@classmethod
22+
def construct_assume_role_policy_for_service_principal(cls, service_principal):
23+
document = {
24+
"Version": "2012-10-17",
25+
"Statement": [
26+
{"Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": {"Service": [service_principal]},}
27+
],
28+
}
29+
return document
30+
31+
@classmethod
32+
def step_functions_start_execution_role_policy(cls, state_machine_arn, logical_id):
33+
document = {
34+
"PolicyName": logical_id + "StartExecutionPolicy",
35+
"PolicyDocument": {
36+
"Statement": [{"Action": "states:StartExecution", "Effect": "Allow", "Resource": state_machine_arn}]
37+
},
38+
}
39+
return document
40+
41+
@classmethod
42+
def stepfunctions_assume_role_policy(cls):
43+
document = {
44+
"Version": "2012-10-17",
45+
"Statement": [
46+
{"Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": {"Service": ["states.amazonaws.com"]},}
47+
],
48+
}
49+
return document
50+
2151
@classmethod
2252
def cloud_watch_log_assume_role_policy(cls):
2353
document = {
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
from enum import Enum
2+
from collections import namedtuple
3+
4+
from six import string_types
5+
6+
from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_if, is_intrinsic_no_value
7+
from samtranslator.model.exceptions import InvalidTemplateException
8+
9+
PolicyEntry = namedtuple("PolicyEntry", "data type")
10+
11+
12+
class ResourcePolicies(object):
13+
"""
14+
Class encapsulating the policies property of SAM resources. This class strictly encapsulates the data
15+
and does not take opinions on how to handle them.
16+
17+
There are three types of policies:
18+
- Policy Statements
19+
- AWS or Custom Managed Policy names/arns
20+
- Policy Templates
21+
22+
This class is capable of parsing and detecting the type of the policy. Optionally, if policy template information
23+
is provided to this class, it will detect Policy Templates too.
24+
"""
25+
26+
POLICIES_PROPERTY_NAME = "Policies"
27+
28+
def __init__(self, resource_properties, policy_template_processor=None):
29+
"""
30+
Initialize with policies data from resource's properties
31+
32+
:param dict resource_properties: Dictionary containing properties of this resource
33+
:param policy_template_processor: Optional Instance of PolicyTemplateProcessor that can conclusively detect
34+
if a given policy is a template or not. If not provided, then this class will not detect policy templates.
35+
"""
36+
37+
# This variable is required to get policies
38+
self._policy_template_processor = policy_template_processor
39+
40+
# Build the list of policies upon construction.
41+
self.policies = self._get_policies(resource_properties)
42+
43+
def get(self):
44+
"""
45+
Iterator method that "yields" the next policy entry on subsequent calls to this method.
46+
47+
:yields namedtuple("data", "type"): Yields a named tuple containing the policy data and its type
48+
"""
49+
50+
for policy_tuple in self.policies:
51+
yield policy_tuple
52+
53+
def __len__(self):
54+
return len(self.policies)
55+
56+
def _get_policies(self, resource_properties):
57+
"""
58+
Returns a list of policies from the resource properties. This method knows how to interpret and handle
59+
polymorphic nature of the policies property.
60+
61+
Policies can be one of the following:
62+
63+
* Managed policy name: string
64+
* List of managed policy names: list of strings
65+
* IAM Policy document: dict containing Statement key
66+
* List of IAM Policy documents: list of IAM Policy Document
67+
* Policy Template: dict with only one key where key is in list of supported policy template names
68+
* List of Policy Templates: list of Policy Template
69+
70+
71+
:param dict resource_properties: Dictionary of resource properties containing the policies property.
72+
It is assumed that this is already a dictionary and contains policies key.
73+
:return list of PolicyEntry: List of policies, where each item is an instance of named tuple `PolicyEntry`
74+
"""
75+
76+
policies = None
77+
78+
if self._contains_policies(resource_properties):
79+
policies = resource_properties[self.POLICIES_PROPERTY_NAME]
80+
81+
if not policies:
82+
# Policies is None or empty
83+
return []
84+
85+
if not isinstance(policies, list):
86+
# Just a single entry. Make it into a list of convenience
87+
policies = [policies]
88+
89+
result = []
90+
for policy in policies:
91+
policy_type = self._get_type(policy)
92+
entry = PolicyEntry(data=policy, type=policy_type)
93+
result.append(entry)
94+
95+
return result
96+
97+
def _contains_policies(self, resource_properties):
98+
"""
99+
Is there policies data in this resource?
100+
101+
:param dict resource_properties: Properties of the resource
102+
:return: True if we can process this resource. False, otherwise
103+
"""
104+
return (
105+
resource_properties is not None
106+
and isinstance(resource_properties, dict)
107+
and self.POLICIES_PROPERTY_NAME in resource_properties
108+
)
109+
110+
def _get_type(self, policy):
111+
"""
112+
Returns the type of the given policy
113+
114+
:param string or dict policy: Policy data
115+
:return PolicyTypes: Type of the given policy. None, if type could not be inferred
116+
"""
117+
118+
# Must handle intrinsic functions. Policy could be a primitive type or an intrinsic function
119+
120+
# Managed policies are of type string
121+
if isinstance(policy, string_types):
122+
return PolicyTypes.MANAGED_POLICY
123+
124+
# Handle the special case for 'if' intrinsic function
125+
if is_intrinsic_if(policy):
126+
return self._get_type_from_intrinsic_if(policy)
127+
128+
# Intrinsic functions are treated as managed policies by default
129+
if is_intrinsic(policy):
130+
return PolicyTypes.MANAGED_POLICY
131+
132+
# Policy statement is a dictionary with the key "Statement" in it
133+
if isinstance(policy, dict) and "Statement" in policy:
134+
return PolicyTypes.POLICY_STATEMENT
135+
136+
# This could be a policy template then.
137+
if self._is_policy_template(policy):
138+
return PolicyTypes.POLICY_TEMPLATE
139+
140+
# Nothing matches. Don't take opinions on how to handle it. Instead just set the appropriate type.
141+
return PolicyTypes.UNKNOWN
142+
143+
def _is_policy_template(self, policy):
144+
"""
145+
Is the given policy data a policy template? Policy templates is a dictionary with one key which is the name
146+
of the template.
147+
148+
:param dict policy: Policy data
149+
:return: True, if this is a policy template. False if it is not
150+
"""
151+
152+
return (
153+
self._policy_template_processor is not None
154+
and isinstance(policy, dict)
155+
and len(policy) == 1
156+
and self._policy_template_processor.has(list(policy.keys())[0]) is True
157+
)
158+
159+
def _get_type_from_intrinsic_if(self, policy):
160+
"""
161+
Returns the type of the given policy assuming that it is an intrinsic if function
162+
163+
:param policy: Input value to get type from
164+
:return: PolicyTypes: Type of the given policy. PolicyTypes.UNKNOWN, if type could not be inferred
165+
"""
166+
intrinsic_if_value = policy["Fn::If"]
167+
168+
if not len(intrinsic_if_value) == 3:
169+
raise InvalidTemplateException("Fn::If requires 3 arguments")
170+
171+
if_data = intrinsic_if_value[1]
172+
else_data = intrinsic_if_value[2]
173+
174+
if_data_type = self._get_type(if_data)
175+
else_data_type = self._get_type(else_data)
176+
177+
if if_data_type == else_data_type:
178+
return if_data_type
179+
180+
if is_intrinsic_no_value(if_data):
181+
return else_data_type
182+
183+
if is_intrinsic_no_value(else_data):
184+
return if_data_type
185+
186+
raise InvalidTemplateException(
187+
"Different policy types within the same Fn::If statement is unsupported. "
188+
"Separate different policy types into different Fn::If statements"
189+
)
190+
191+
192+
class PolicyTypes(Enum):
193+
"""
194+
Enum of different policy types supported by SAM & this plugin
195+
"""
196+
197+
MANAGED_POLICY = "managed_policy"
198+
POLICY_STATEMENT = "policy_statement"
199+
POLICY_TEMPLATE = "policy_template"
200+
UNKNOWN = "unknown"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .role_constructor import construct_role_for_resource
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from six import string_types
2+
3+
from samtranslator.model.iam import IAMRole
4+
from samtranslator.model.resource_policies import ResourcePolicies, PolicyTypes
5+
from samtranslator.model.intrinsics import is_intrinsic_if, is_intrinsic_no_value
6+
from samtranslator.model.exceptions import InvalidResourceException
7+
8+
9+
def construct_role_for_resource(
10+
resource_logical_id,
11+
attributes,
12+
managed_policy_map,
13+
assume_role_policy_document,
14+
resource_policies,
15+
managed_policy_arns=None,
16+
policy_documents=None,
17+
permissions_boundary=None,
18+
tags=None,
19+
):
20+
"""
21+
Constructs an execution role for a resource.
22+
:param resource_logical_id: The logical_id of the SAM resource that the role will be associated with
23+
:param attributes: Map of resource attributes to their values
24+
:param managed_policy_map: Map of managed policy names to the ARNs
25+
:param assume_role_policy_document: The trust policy that must be associated with the role
26+
:param resource_policies: ResourcePolicies object encapuslating the policies property of SAM resource
27+
:param managed_policy_arns: List of managed policy ARNs to be associated with the role
28+
:param policy_documents: List of policy documents to be associated with the role
29+
:param permissions_boundary: The ARN of the policy used to set the permissions boundary for the role
30+
:param tags: Tags to be associated with the role
31+
32+
:returns: the generated IAM Role
33+
:rtype: model.iam.IAMRole
34+
"""
35+
role_logical_id = resource_logical_id + "Role"
36+
execution_role = IAMRole(logical_id=role_logical_id, attributes=attributes)
37+
execution_role.AssumeRolePolicyDocument = assume_role_policy_document
38+
39+
if not managed_policy_arns:
40+
managed_policy_arns = []
41+
42+
if not policy_documents:
43+
policy_documents = []
44+
45+
for index, policy_entry in enumerate(resource_policies.get()):
46+
if policy_entry.type is PolicyTypes.POLICY_STATEMENT:
47+
48+
if is_intrinsic_if(policy_entry.data):
49+
50+
intrinsic_if = policy_entry.data
51+
then_statement = intrinsic_if["Fn::If"][1]
52+
else_statement = intrinsic_if["Fn::If"][2]
53+
54+
if not is_intrinsic_no_value(then_statement):
55+
then_statement = {
56+
"PolicyName": execution_role.logical_id + "Policy" + str(index),
57+
"PolicyDocument": then_statement,
58+
}
59+
intrinsic_if["Fn::If"][1] = then_statement
60+
61+
if not is_intrinsic_no_value(else_statement):
62+
else_statement = {
63+
"PolicyName": execution_role.logical_id + "Policy" + str(index),
64+
"PolicyDocument": else_statement,
65+
}
66+
intrinsic_if["Fn::If"][2] = else_statement
67+
68+
policy_documents.append(intrinsic_if)
69+
70+
else:
71+
policy_documents.append(
72+
{
73+
"PolicyName": execution_role.logical_id + "Policy" + str(index),
74+
"PolicyDocument": policy_entry.data,
75+
}
76+
)
77+
78+
elif policy_entry.type is PolicyTypes.MANAGED_POLICY:
79+
80+
# There are three options:
81+
# Managed Policy Name (string): Try to convert to Managed Policy ARN
82+
# Managed Policy Arn (string): Insert it directly into the list
83+
# Intrinsic Function (dict): Insert it directly into the list
84+
#
85+
# When you insert into managed_policy_arns list, de-dupe to prevent same ARN from showing up twice
86+
#
87+
88+
policy_arn = policy_entry.data
89+
if isinstance(policy_entry.data, string_types) and policy_entry.data in managed_policy_map:
90+
policy_arn = managed_policy_map[policy_entry.data]
91+
92+
# De-Duplicate managed policy arns before inserting. Mainly useful
93+
# when customer specifies a managed policy which is already inserted
94+
# by SAM, such as AWSLambdaBasicExecutionRole
95+
if policy_arn not in managed_policy_arns:
96+
managed_policy_arns.append(policy_arn)
97+
else:
98+
# Policy Templates are not supported here in the "core"
99+
raise InvalidResourceException(
100+
resource_logical_id,
101+
"Policy at index {} in the '{}' property is not valid".format(
102+
index, resource_policies.POLICIES_PROPERTY_NAME
103+
),
104+
)
105+
106+
execution_role.ManagedPolicyArns = list(managed_policy_arns)
107+
execution_role.Policies = policy_documents or None
108+
execution_role.PermissionsBoundary = permissions_boundary
109+
execution_role.Tags = tags
110+
111+
return execution_role

0 commit comments

Comments
 (0)