Skip to content

Commit 4af83fa

Browse files
committed
Adding support for nested results on Fn:GetAtt
1 parent 46c2414 commit 4af83fa

File tree

4 files changed

+90
-1
lines changed

4 files changed

+90
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ dist/*
55
*.egg-info/*
66
.run
77
.tox
8-
8+
.venv
99
__pycache__

cfn_policy_validator/parsers/utils/intrinsic_functions/fn_get_att_evaluator.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ def evaluate(self, get_att_lookup, visited_nodes=None):
9393
# IAM policy
9494
properties = resource.get('Properties', {})
9595
property_value = properties.get(attribute_name)
96+
97+
# Support nested GetAtt attributes (e.g. MasterUserSecret.SecretArn, which is split as a dotted attribute name).
98+
# Traverse into nested properties when the attribute contains a dot.
99+
if property_value is None and '.' in attribute_name:
100+
property_value = self._resolve_nested_property(properties, attribute_name)
101+
96102
if property_value is None:
97103
raise ApplicationError(f'Call to GetAtt not supported for: {logical_name_of_resource}.{attribute_name}')
98104

@@ -103,6 +109,22 @@ def evaluate(self, get_att_lookup, visited_nodes=None):
103109
# there are many return types for GetAtt, so it's the caller's responsibility to validate expected type
104110
return self.node_evaluator.eval(property_value, visited_nodes=visited_nodes)
105111

112+
@staticmethod
113+
def _resolve_nested_property(properties, attribute_name):
114+
"""Resolve dotted attribute names by traversing nested properties.
115+
For example, 'MasterUserSecret.SecretArn' looks up properties['MasterUserSecret']['SecretArn'].
116+
"""
117+
parts = attribute_name.split('.')
118+
current = properties
119+
for part in parts:
120+
if isinstance(current, dict):
121+
current = current.get(part)
122+
else:
123+
return None
124+
if current is None:
125+
return None
126+
return current
127+
106128
def get_code_artifact_name(self, region, resource_name, resource):
107129
properties = resource.get('Properties',[])
108130
return properties.get('DomainName')

cfn_policy_validator/tests/cfn_tools_tests/test_yaml_parser.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,12 @@ def test_dates_are_parsed_to_string(self):
122122
'Version': '2012-10-21'
123123
}
124124
self.assert_parsed_result_is_expected(expected_template, raw_template)
125+
126+
def test_get_att_shorthand_with_nested_dotted_attribute_is_replaced(self):
127+
raw_template = """
128+
!GetAtt RdsDbCluster.MasterUserSecret.SecretArn
129+
"""
130+
expected_template = {
131+
'Fn::GetAtt': ['RdsDbCluster', 'MasterUserSecret.SecretArn']
132+
}
133+
self.assert_parsed_result_is_expected(expected_template, raw_template)

cfn_policy_validator/tests/parsers_tests/utils_tests/intrinsic_functions_tests/test_get_att_evaluator.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,61 @@ def test_raises_an_error(self):
233233
node_evaluator.eval(template['Resources']['ResourceA']['Properties']['RoleName'])
234234

235235
self.assertEqual("Additional items are not allowed ('Value2' was unexpected), Path: Fn::GetAtt", str(cm.exception))
236+
237+
238+
class WhenEvaluatingAPolicyWithAGetAttForANestedDottedAttribute(unittest.TestCase):
239+
"""Reproduces the customer issue where !GetAtt RdsDbCluster.MasterUserSecret.SecretArn
240+
fails with 'Call to GetAtt not supported'."""
241+
242+
@mock_node_evaluator_setup()
243+
def test_returns_nested_property_value(self):
244+
template = load_resources({
245+
'RdsDbCluster': {
246+
'Type': 'AWS::RDS::DBCluster',
247+
'Properties': {
248+
'MasterUserSecret': {
249+
'SecretArn': 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret'
250+
}
251+
}
252+
},
253+
'ResourceA': {
254+
'Type': 'AWS::Random::Service',
255+
'Properties': {
256+
'PolicyProperty': {
257+
'Fn::GetAtt': ['RdsDbCluster', 'MasterUserSecret.SecretArn']
258+
}
259+
}
260+
}
261+
})
262+
263+
node_evaluator = build_node_evaluator(template)
264+
265+
result = node_evaluator.eval(template['Resources']['ResourceA']['Properties']['PolicyProperty'])
266+
self.assertEqual('arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret', result)
267+
268+
@mock_node_evaluator_setup()
269+
def test_raises_exception_when_nested_property_does_not_exist(self):
270+
template = load_resources({
271+
'RdsDbCluster': {
272+
'Type': 'AWS::RDS::DBCluster',
273+
'Properties': {
274+
'MasterUserSecret': {
275+
'OtherProp': 'value'
276+
}
277+
}
278+
},
279+
'ResourceA': {
280+
'Type': 'AWS::Random::Service',
281+
'Properties': {
282+
'PolicyProperty': {
283+
'Fn::GetAtt': ['RdsDbCluster', 'MasterUserSecret.SecretArn']
284+
}
285+
}
286+
}
287+
})
288+
289+
node_evaluator = build_node_evaluator(template)
290+
291+
with self.assertRaises(ApplicationError) as context:
292+
node_evaluator.eval(template['Resources']['ResourceA']['Properties']['PolicyProperty'])
293+
self.assertEqual('Call to GetAtt not supported for: RdsDbCluster.MasterUserSecret.SecretArn', str(context.exception))

0 commit comments

Comments
 (0)