55
66
77import ast
8+ from collections .abc import Mapping
89from itertools import islice , chain
910from types import GeneratorType
1011
12+ from ansible .module_utils .common .collections import is_sequence
1113from ansible .module_utils .common .text .converters import to_text
1214from ansible .module_utils .six import string_types
1315from ansible .parsing .yaml .objects import AnsibleVaultEncryptedUnicode
1416from ansible .utils .native_jinja import NativeJinjaText
17+ from ansible .utils .unsafe_proxy import wrap_var
18+ import ansible .module_utils .compat .typing as t
19+
20+ from jinja2 .runtime import StrictUndefined
1521
1622
1723_JSON_MAP = {
@@ -28,6 +34,40 @@ def visit_Name(self, node):
2834 return ast .Constant (value = _JSON_MAP [node .id ])
2935
3036
37+ def _is_unsafe (value : t .Any ) -> bool :
38+ """
39+ Our helper function, which will also recursively check dict and
40+ list entries due to the fact that they may be repr'd and contain
41+ a key or value which contains jinja2 syntax and would otherwise
42+ lose the AnsibleUnsafe value.
43+ """
44+ to_check = [value ]
45+ seen = set ()
46+
47+ while True :
48+ if not to_check :
49+ break
50+
51+ val = to_check .pop (0 )
52+ val_id = id (val )
53+
54+ if val_id in seen :
55+ continue
56+ seen .add (val_id )
57+
58+ if isinstance (val , AnsibleUndefined ):
59+ continue
60+ if isinstance (val , Mapping ):
61+ to_check .extend (val .keys ())
62+ to_check .extend (val .values ())
63+ elif is_sequence (val ):
64+ to_check .extend (val )
65+ elif getattr (val , '__UNSAFE__' , False ):
66+ return True
67+
68+ return False
69+
70+
3171def ansible_eval_concat (nodes ):
3272 """Return a string of concatenated compiled nodes. Throw an undefined error
3373 if any of the nodes is undefined.
@@ -43,17 +83,28 @@ def ansible_eval_concat(nodes):
4383 if not head :
4484 return ''
4585
86+ unsafe = False
87+
4688 if len (head ) == 1 :
4789 out = head [0 ]
4890
4991 if isinstance (out , NativeJinjaText ):
5092 return out
5193
94+ unsafe = _is_unsafe (out )
5295 out = to_text (out )
5396 else :
5497 if isinstance (nodes , GeneratorType ):
5598 nodes = chain (head , nodes )
56- out = '' .join ([to_text (v ) for v in nodes ])
99+
100+ out_values = []
101+ for v in nodes :
102+ if not unsafe and _is_unsafe (v ):
103+ unsafe = True
104+
105+ out_values .append (to_text (v ))
106+
107+ out = '' .join (out_values )
57108
58109 # if this looks like a dictionary, list or bool, convert it to such
59110 if out .startswith (('{' , '[' )) or out in ('True' , 'False' ):
@@ -68,6 +119,9 @@ def ansible_eval_concat(nodes):
68119 except (TypeError , ValueError , SyntaxError , MemoryError ):
69120 pass
70121
122+ if unsafe :
123+ out = wrap_var (out )
124+
71125 return out
72126
73127
@@ -78,7 +132,19 @@ def ansible_concat(nodes):
78132
79133 Used in Templar.template() when jinja2_native=False and convert_data=False.
80134 """
81- return '' .join ([to_text (v ) for v in nodes ])
135+ unsafe = False
136+ values = []
137+ for v in nodes :
138+ if not unsafe and _is_unsafe (v ):
139+ unsafe = True
140+
141+ values .append (to_text (v ))
142+
143+ out = '' .join (values )
144+ if unsafe :
145+ out = wrap_var (out )
146+
147+ return out
82148
83149
84150def ansible_native_concat (nodes ):
@@ -95,6 +161,8 @@ def ansible_native_concat(nodes):
95161 if not head :
96162 return None
97163
164+ unsafe = False
165+
98166 if len (head ) == 1 :
99167 out = head [0 ]
100168
@@ -115,10 +183,21 @@ def ansible_native_concat(nodes):
115183 # short-circuit literal_eval for anything other than strings
116184 if not isinstance (out , string_types ):
117185 return out
186+
187+ unsafe = _is_unsafe (out )
188+
118189 else :
119190 if isinstance (nodes , GeneratorType ):
120191 nodes = chain (head , nodes )
121- out = '' .join ([to_text (v ) for v in nodes ])
192+
193+ out_values = []
194+ for v in nodes :
195+ if not unsafe and _is_unsafe (v ):
196+ unsafe = True
197+
198+ out_values .append (to_text (v ))
199+
200+ out = '' .join (out_values )
122201
123202 try :
124203 evaled = ast .literal_eval (
@@ -128,10 +207,45 @@ def ansible_native_concat(nodes):
128207 ast .parse (out , mode = 'eval' )
129208 )
130209 except (TypeError , ValueError , SyntaxError , MemoryError ):
210+ if unsafe :
211+ out = wrap_var (out )
212+
131213 return out
132214
133215 if isinstance (evaled , string_types ):
134216 quote = out [0 ]
135- return f'{ quote } { evaled } { quote } '
217+ evaled = f'{ quote } { evaled } { quote } '
218+
219+ if unsafe :
220+ evaled = wrap_var (evaled )
136221
137222 return evaled
223+
224+
225+ class AnsibleUndefined (StrictUndefined ):
226+ """
227+ A custom Undefined class, which returns further Undefined objects on access,
228+ rather than throwing an exception.
229+ """
230+ def __getattr__ (self , name ):
231+ if name == '__UNSAFE__' :
232+ # AnsibleUndefined should never be assumed to be unsafe
233+ # This prevents ``hasattr(val, '__UNSAFE__')`` from evaluating to ``True``
234+ raise AttributeError (name )
235+ # Return original Undefined object to preserve the first failure context
236+ return self
237+
238+ def __getitem__ (self , key ):
239+ # Return original Undefined object to preserve the first failure context
240+ return self
241+
242+ def __repr__ (self ):
243+ return 'AnsibleUndefined(hint={0!r}, obj={1!r}, name={2!r})' .format (
244+ self ._undefined_hint ,
245+ self ._undefined_obj ,
246+ self ._undefined_name
247+ )
248+
249+ def __contains__ (self , item ):
250+ # Return original Undefined object to preserve the first failure context
251+ return self
0 commit comments