7
7
8
8
9
9
import ast
10
+ from collections .abc import Mapping
10
11
from itertools import islice , chain
11
12
from types import GeneratorType
12
13
14
+ from ansible .module_utils .common .collections import is_sequence
13
15
from ansible .module_utils .common .text .converters import to_text
14
16
from ansible .module_utils .six import string_types
15
17
from ansible .parsing .yaml .objects import AnsibleVaultEncryptedUnicode
16
18
from ansible .utils .native_jinja import NativeJinjaText
19
+ from ansible .utils .unsafe_proxy import wrap_var
20
+ import ansible .module_utils .compat .typing as t
21
+
22
+ from jinja2 .runtime import StrictUndefined
17
23
18
24
19
25
_JSON_MAP = {
@@ -30,6 +36,40 @@ def visit_Name(self, node):
30
36
return ast .Constant (value = _JSON_MAP [node .id ])
31
37
32
38
39
+ def _is_unsafe (value : t .Any ) -> bool :
40
+ """
41
+ Our helper function, which will also recursively check dict and
42
+ list entries due to the fact that they may be repr'd and contain
43
+ a key or value which contains jinja2 syntax and would otherwise
44
+ lose the AnsibleUnsafe value.
45
+ """
46
+ to_check = [value ]
47
+ seen = set ()
48
+
49
+ while True :
50
+ if not to_check :
51
+ break
52
+
53
+ val = to_check .pop (0 )
54
+ val_id = id (val )
55
+
56
+ if val_id in seen :
57
+ continue
58
+ seen .add (val_id )
59
+
60
+ if isinstance (val , AnsibleUndefined ):
61
+ continue
62
+ if isinstance (val , Mapping ):
63
+ to_check .extend (val .keys ())
64
+ to_check .extend (val .values ())
65
+ elif is_sequence (val ):
66
+ to_check .extend (val )
67
+ elif getattr (val , '__UNSAFE__' , False ):
68
+ return True
69
+
70
+ return False
71
+
72
+
33
73
def ansible_eval_concat (nodes ):
34
74
"""Return a string of concatenated compiled nodes. Throw an undefined error
35
75
if any of the nodes is undefined.
@@ -45,17 +85,28 @@ def ansible_eval_concat(nodes):
45
85
if not head :
46
86
return ''
47
87
88
+ unsafe = False
89
+
48
90
if len (head ) == 1 :
49
91
out = head [0 ]
50
92
51
93
if isinstance (out , NativeJinjaText ):
52
94
return out
53
95
96
+ unsafe = _is_unsafe (out )
54
97
out = to_text (out )
55
98
else :
56
99
if isinstance (nodes , GeneratorType ):
57
100
nodes = chain (head , nodes )
58
- out = '' .join ([to_text (v ) for v in nodes ])
101
+
102
+ out_values = []
103
+ for v in nodes :
104
+ if not unsafe and _is_unsafe (v ):
105
+ unsafe = True
106
+
107
+ out_values .append (to_text (v ))
108
+
109
+ out = '' .join (out_values )
59
110
60
111
# if this looks like a dictionary, list or bool, convert it to such
61
112
if out .startswith (('{' , '[' )) or out in ('True' , 'False' ):
@@ -70,6 +121,9 @@ def ansible_eval_concat(nodes):
70
121
except (TypeError , ValueError , SyntaxError , MemoryError ):
71
122
pass
72
123
124
+ if unsafe :
125
+ out = wrap_var (out )
126
+
73
127
return out
74
128
75
129
@@ -80,7 +134,19 @@ def ansible_concat(nodes):
80
134
81
135
Used in Templar.template() when jinja2_native=False and convert_data=False.
82
136
"""
83
- return '' .join ([to_text (v ) for v in nodes ])
137
+ unsafe = False
138
+ values = []
139
+ for v in nodes :
140
+ if not unsafe and _is_unsafe (v ):
141
+ unsafe = True
142
+
143
+ values .append (to_text (v ))
144
+
145
+ out = '' .join (values )
146
+ if unsafe :
147
+ out = wrap_var (out )
148
+
149
+ return out
84
150
85
151
86
152
def ansible_native_concat (nodes ):
@@ -97,6 +163,8 @@ def ansible_native_concat(nodes):
97
163
if not head :
98
164
return None
99
165
166
+ unsafe = False
167
+
100
168
if len (head ) == 1 :
101
169
out = head [0 ]
102
170
@@ -117,10 +185,21 @@ def ansible_native_concat(nodes):
117
185
# short-circuit literal_eval for anything other than strings
118
186
if not isinstance (out , string_types ):
119
187
return out
188
+
189
+ unsafe = _is_unsafe (out )
190
+
120
191
else :
121
192
if isinstance (nodes , GeneratorType ):
122
193
nodes = chain (head , nodes )
123
- out = '' .join ([to_text (v ) for v in nodes ])
194
+
195
+ out_values = []
196
+ for v in nodes :
197
+ if not unsafe and _is_unsafe (v ):
198
+ unsafe = True
199
+
200
+ out_values .append (to_text (v ))
201
+
202
+ out = '' .join (out_values )
124
203
125
204
try :
126
205
evaled = ast .literal_eval (
@@ -130,10 +209,45 @@ def ansible_native_concat(nodes):
130
209
ast .parse (out , mode = 'eval' )
131
210
)
132
211
except (TypeError , ValueError , SyntaxError , MemoryError ):
212
+ if unsafe :
213
+ out = wrap_var (out )
214
+
133
215
return out
134
216
135
217
if isinstance (evaled , string_types ):
136
218
quote = out [0 ]
137
- return f'{ quote } { evaled } { quote } '
219
+ evaled = f'{ quote } { evaled } { quote } '
220
+
221
+ if unsafe :
222
+ evaled = wrap_var (evaled )
138
223
139
224
return evaled
225
+
226
+
227
+ class AnsibleUndefined (StrictUndefined ):
228
+ """
229
+ A custom Undefined class, which returns further Undefined objects on access,
230
+ rather than throwing an exception.
231
+ """
232
+ def __getattr__ (self , name ):
233
+ if name == '__UNSAFE__' :
234
+ # AnsibleUndefined should never be assumed to be unsafe
235
+ # This prevents ``hasattr(val, '__UNSAFE__')`` from evaluating to ``True``
236
+ raise AttributeError (name )
237
+ # Return original Undefined object to preserve the first failure context
238
+ return self
239
+
240
+ def __getitem__ (self , key ):
241
+ # Return original Undefined object to preserve the first failure context
242
+ return self
243
+
244
+ def __repr__ (self ):
245
+ return 'AnsibleUndefined(hint={0!r}, obj={1!r}, name={2!r})' .format (
246
+ self ._undefined_hint ,
247
+ self ._undefined_obj ,
248
+ self ._undefined_name
249
+ )
250
+
251
+ def __contains__ (self , item ):
252
+ # Return original Undefined object to preserve the first failure context
253
+ return self
0 commit comments