cli2 4.2.9__tar.gz → 4.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli2-4.2.9/cli2.egg-info → cli2-4.3.0}/PKG-INFO +1 -1
- {cli2-4.2.9 → cli2-4.3.0}/cli2/__init__.py +1 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/ansible/action.py +41 -81
- {cli2-4.2.9 → cli2-4.3.0}/cli2/client.py +39 -61
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/client.py +1 -1
- cli2-4.3.0/cli2/mask.py +121 -0
- {cli2-4.2.9 → cli2-4.3.0/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-4.2.9 → cli2-4.3.0}/cli2.egg-info/SOURCES.txt +2 -0
- {cli2-4.2.9 → cli2-4.3.0}/setup.py +1 -1
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_ansible.py +3 -3
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_client.py +27 -23
- cli2-4.3.0/tests/test_mask.py +22 -0
- {cli2-4.2.9 → cli2-4.3.0}/MANIFEST.in +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/README.rst +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/classifiers.txt +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/ansible/__init__.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/ansible/playbook.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/ansible/variables.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/asyncio.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/cli.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/cli2.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/colors.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/configuration.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/decorators.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/display.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/__init__.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/conf.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/example.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/example_obj.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/nesting.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/obj.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/obj2.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/examples/test.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/lock.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/log.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/node.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/sphinx.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/table.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2/test.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2.egg-info/requires.txt +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/setup.cfg +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_ansible_variables.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_asyncio.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_cli.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_command.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_configuration.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_decorators.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_display.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_entry_point.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_group.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_inject.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_lock.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_node.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_restful.py +0 -0
- {cli2-4.2.9 → cli2-4.3.0}/tests/test_table.py +0 -0
|
@@ -130,37 +130,22 @@ class ActionBase(ActionBase):
|
|
|
130
130
|
The client object generated by :py:meth:`client_factory` if you
|
|
131
131
|
implement it.
|
|
132
132
|
|
|
133
|
-
.. py:attribute::
|
|
134
|
-
|
|
135
|
-
List of result keys to mask, if set, this module will print the result
|
|
136
|
-
with masked values, so you can set no_log: True for the task and still
|
|
137
|
-
see most of the result.
|
|
133
|
+
.. py:attribute:: mask_keys
|
|
138
134
|
|
|
139
|
-
|
|
135
|
+
Declare a list of keys to mask:
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
the `mask` fact + those in the client if any.
|
|
137
|
+
.. code-block:: python
|
|
143
138
|
|
|
144
|
-
|
|
139
|
+
class AnsibleModule(ansible.ActionBase):
|
|
140
|
+
mask_keys = ['secret', 'password']
|
|
145
141
|
|
|
146
|
-
|
|
147
|
-
which are in :py:attr:`masked_keys`.
|
|
142
|
+
.. py:attribute:: mask
|
|
148
143
|
|
|
149
|
-
|
|
144
|
+
:py:class:`~cli2.mask.Mask` object
|
|
150
145
|
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@property
|
|
156
|
-
def masked_keys(self):
|
|
157
|
-
result = []
|
|
158
|
-
if self.mask:
|
|
159
|
-
result += self.mask
|
|
160
|
-
result += self.masked
|
|
161
|
-
if self.client and self.client.mask:
|
|
162
|
-
result += self.client.mask
|
|
163
|
-
return result
|
|
146
|
+
masked_keys = Option(fact='mask_keys', default=[])
|
|
147
|
+
masked_values = Option(fact='mask_values', default=[])
|
|
148
|
+
mask_keys = None
|
|
164
149
|
|
|
165
150
|
def get(self, arg_name=None, fact_name=None, default=UNSET_DEFAULT):
|
|
166
151
|
if arg_name and arg_name in self._task.args:
|
|
@@ -181,23 +166,31 @@ class ActionBase(ActionBase):
|
|
|
181
166
|
asyncio.run(self.run_wrapped_async())
|
|
182
167
|
return self.result
|
|
183
168
|
|
|
184
|
-
def
|
|
185
|
-
#
|
|
186
|
-
self.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
169
|
+
def mask_init(self):
|
|
170
|
+
# use ansible template value renderer
|
|
171
|
+
self.mask.renderer = lambda value: self._templar.template(value)
|
|
172
|
+
|
|
173
|
+
keys = self.masked_keys + (self.mask_keys or [])
|
|
174
|
+
for key in keys:
|
|
175
|
+
self.mask.keys.add(key)
|
|
176
|
+
|
|
177
|
+
# discover values from facts
|
|
178
|
+
if value := self.task_vars.get(key, None):
|
|
179
|
+
self.mask.values.add(value)
|
|
180
|
+
|
|
181
|
+
# discover value from args
|
|
182
|
+
if value := self._task.args.get(key, None):
|
|
183
|
+
self.mask.values.add(value)
|
|
184
|
+
|
|
185
|
+
# discover values to mask
|
|
186
|
+
for value in self.masked_values:
|
|
187
|
+
if value:
|
|
188
|
+
self.mask.values.add(value)
|
|
196
189
|
|
|
197
190
|
async def run_wrapped_async(self):
|
|
198
191
|
self.verbosity = self.task_vars.get('ansible_verbosity', 0)
|
|
199
192
|
|
|
200
|
-
if 'LOG_LEVEL' not in os.environ and
|
|
193
|
+
if 'LOG_LEVEL' not in os.environ and 'DEBUG' not in os.environ:
|
|
201
194
|
if self.verbosity == 1:
|
|
202
195
|
os.environ['LOG_LEVEL'] = 'INFO'
|
|
203
196
|
elif self.verbosity >= 2:
|
|
@@ -209,7 +202,10 @@ class ActionBase(ActionBase):
|
|
|
209
202
|
self.client = await self.client_factory()
|
|
210
203
|
except NotImplementedError:
|
|
211
204
|
self.client = None
|
|
212
|
-
|
|
205
|
+
self.mask = cli2.Mask()
|
|
206
|
+
else:
|
|
207
|
+
self.mask = copy.deepcopy(self.client.mask)
|
|
208
|
+
self.mask_init()
|
|
213
209
|
await self.run_async()
|
|
214
210
|
except Exception as exc:
|
|
215
211
|
self.result['failed'] = True
|
|
@@ -244,10 +240,10 @@ class ActionBase(ActionBase):
|
|
|
244
240
|
):
|
|
245
241
|
diff = difflib.unified_diff(
|
|
246
242
|
to_nice_yaml(
|
|
247
|
-
self.
|
|
243
|
+
self.mask(self._before_data)
|
|
248
244
|
).splitlines(),
|
|
249
245
|
to_nice_yaml(
|
|
250
|
-
self.
|
|
246
|
+
self.mask(self._after_data)
|
|
251
247
|
).splitlines(),
|
|
252
248
|
self._before_label,
|
|
253
249
|
self._after_label,
|
|
@@ -258,7 +254,7 @@ class ActionBase(ActionBase):
|
|
|
258
254
|
"""
|
|
259
255
|
Render data as masked yaml.
|
|
260
256
|
"""
|
|
261
|
-
data = self.
|
|
257
|
+
data = self.mask(data)
|
|
262
258
|
yaml = to_nice_yaml(data)
|
|
263
259
|
rendered = cli2.yaml_highlight(yaml)
|
|
264
260
|
self.print(rendered)
|
|
@@ -267,42 +263,6 @@ class ActionBase(ActionBase):
|
|
|
267
263
|
# this serves for mocking
|
|
268
264
|
print(data)
|
|
269
265
|
|
|
270
|
-
def mask_data(self, data):
|
|
271
|
-
""""
|
|
272
|
-
Do our best to mask sensitive values in the data param recursively.
|
|
273
|
-
|
|
274
|
-
- when data is a dict: it is recursively iterated on, any value that in
|
|
275
|
-
is :py:attr:`masked_keys` will have it's value replaced with
|
|
276
|
-
``***MASKED***``, also, the value is added to
|
|
277
|
-
:py:attr:`masked_values`.
|
|
278
|
-
- when data is a string, each :py:attr:`masked_values` will be replaced
|
|
279
|
-
with ``***MASKED***``, so we're actually able to mask sensitive
|
|
280
|
-
information from stdout outputs and the likes.
|
|
281
|
-
- when data is a list, each item is passed to :py:meth:`mask_data()`.
|
|
282
|
-
|
|
283
|
-
Note that the :envvar:`DEBUG` environment variable will prevent any
|
|
284
|
-
masking at all.
|
|
285
|
-
"""
|
|
286
|
-
if os.getenv('DEBUG'):
|
|
287
|
-
return data
|
|
288
|
-
|
|
289
|
-
return self._mask(copy.deepcopy(data))
|
|
290
|
-
|
|
291
|
-
def _mask(self, data):
|
|
292
|
-
if isinstance(data, dict):
|
|
293
|
-
for key, value in data.items():
|
|
294
|
-
if key in self.masked_keys:
|
|
295
|
-
self.masked_values.append(self._templar.template(value))
|
|
296
|
-
data[key] = '***MASKED***'
|
|
297
|
-
else:
|
|
298
|
-
data[key] = self.mask_data(value)
|
|
299
|
-
elif isinstance(data, list):
|
|
300
|
-
return [self.mask_data(item) for item in data]
|
|
301
|
-
elif isinstance(data, str):
|
|
302
|
-
for value in self.masked_values:
|
|
303
|
-
data = data.replace(str(value), '***MASKED***')
|
|
304
|
-
return data
|
|
305
|
-
|
|
306
266
|
async def run_async(self):
|
|
307
267
|
"""
|
|
308
268
|
The method you are supposed to implement.
|
|
@@ -406,7 +366,7 @@ class ActionBase(ActionBase):
|
|
|
406
366
|
if self.verbosity:
|
|
407
367
|
display.display(
|
|
408
368
|
f'<{self.task_vars["inventory_hostname"]}> + '
|
|
409
|
-
+ self.
|
|
369
|
+
+ self.mask(cmd),
|
|
410
370
|
color='blue',
|
|
411
371
|
)
|
|
412
372
|
shell_action = self._shared_loader_obj.action_loader.get(
|
|
@@ -422,9 +382,9 @@ class ActionBase(ActionBase):
|
|
|
422
382
|
|
|
423
383
|
if self.verbosity:
|
|
424
384
|
if 'stderr_lines' in result:
|
|
425
|
-
print(self.
|
|
385
|
+
self.print(self.mask(result['stderr']))
|
|
426
386
|
if 'stdout_lines' in result:
|
|
427
|
-
print(self.
|
|
387
|
+
self.print(self.mask(result['stdout']))
|
|
428
388
|
|
|
429
389
|
result.pop('invocation')
|
|
430
390
|
return result
|
|
@@ -26,6 +26,7 @@ from .asyncio import async_resolve
|
|
|
26
26
|
from .cli import Argument, Command, Group, cmd, hide
|
|
27
27
|
from .colors import colors
|
|
28
28
|
from .log import log
|
|
29
|
+
from .mask import Mask
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class Paginator:
|
|
@@ -850,7 +851,7 @@ class Model(metaclass=ModelMetaclass):
|
|
|
850
851
|
|
|
851
852
|
@property
|
|
852
853
|
def data_masked(self):
|
|
853
|
-
return self.client.
|
|
854
|
+
return self.client.mask(self.data)
|
|
854
855
|
|
|
855
856
|
@classmethod
|
|
856
857
|
@hide('expressions')
|
|
@@ -1098,7 +1099,7 @@ class Handler:
|
|
|
1098
1099
|
self.tries = self.tries_default if tries is None else tries
|
|
1099
1100
|
self.backoff = self.backoff_default if backoff is None else backoff
|
|
1100
1101
|
|
|
1101
|
-
async def __call__(self, client, response, tries,
|
|
1102
|
+
async def __call__(self, client, response, tries, log):
|
|
1102
1103
|
seconds = tries * self.backoff
|
|
1103
1104
|
|
|
1104
1105
|
if isinstance(response, Exception):
|
|
@@ -1126,24 +1127,24 @@ class Handler:
|
|
|
1126
1127
|
return response
|
|
1127
1128
|
|
|
1128
1129
|
if response.status_code in self.refuses:
|
|
1129
|
-
raise RefusedResponseError(client, response, tries
|
|
1130
|
+
raise RefusedResponseError(client, response, tries)
|
|
1130
1131
|
|
|
1131
1132
|
if tries >= self.tries:
|
|
1132
|
-
raise RetriesExceededError(client, response, tries
|
|
1133
|
+
raise RetriesExceededError(client, response, tries)
|
|
1133
1134
|
|
|
1134
1135
|
kwargs = dict(
|
|
1135
1136
|
status_code=response.status_code,
|
|
1136
1137
|
tries=tries,
|
|
1137
1138
|
sleep=seconds,
|
|
1138
1139
|
)
|
|
1139
|
-
key, value = client.response_log_data(response
|
|
1140
|
+
key, value = client.response_log_data(response)
|
|
1140
1141
|
if value:
|
|
1141
1142
|
kwargs[key] = value
|
|
1142
1143
|
|
|
1143
1144
|
if response.status_code in self.retokens:
|
|
1144
1145
|
if tries:
|
|
1145
1146
|
# our authentication is just not working, no need to retry
|
|
1146
|
-
raise TokenGetError(client, response, tries
|
|
1147
|
+
raise TokenGetError(client, response, tries)
|
|
1147
1148
|
log.warn('retoken', **kwargs)
|
|
1148
1149
|
await client.token_reset()
|
|
1149
1150
|
|
|
@@ -1179,11 +1180,10 @@ class ResponseError(ClientError):
|
|
|
1179
1180
|
|
|
1180
1181
|
Request method
|
|
1181
1182
|
"""
|
|
1182
|
-
def __init__(self, client, response, tries,
|
|
1183
|
+
def __init__(self, client, response, tries, msg=None):
|
|
1183
1184
|
self.client = client
|
|
1184
1185
|
self.response = response
|
|
1185
1186
|
self.tries = tries
|
|
1186
|
-
self.mask = mask
|
|
1187
1187
|
self.msg = msg or getattr(self, 'msg', '').format(self=self)
|
|
1188
1188
|
super().__init__(self.enhance(self.msg))
|
|
1189
1189
|
|
|
@@ -1212,10 +1212,7 @@ class ResponseError(ClientError):
|
|
|
1212
1212
|
:param exc: httpx.HTTPStatusError
|
|
1213
1213
|
"""
|
|
1214
1214
|
output = [msg]
|
|
1215
|
-
key, value = self.client.request_log_data(
|
|
1216
|
-
self.response.request,
|
|
1217
|
-
self.mask,
|
|
1218
|
-
)
|
|
1215
|
+
key, value = self.client.request_log_data(self.response.request)
|
|
1219
1216
|
request_msg = ' '.join([
|
|
1220
1217
|
str(self.response.request.method),
|
|
1221
1218
|
str(self.response.request.url),
|
|
@@ -1227,7 +1224,7 @@ class ResponseError(ClientError):
|
|
|
1227
1224
|
if value:
|
|
1228
1225
|
output.append(display.render(value))
|
|
1229
1226
|
|
|
1230
|
-
key, value = self.client.response_log_data(self.response
|
|
1227
|
+
key, value = self.client.response_log_data(self.response)
|
|
1231
1228
|
output.append(
|
|
1232
1229
|
''.join([
|
|
1233
1230
|
colors.bold,
|
|
@@ -1318,9 +1315,14 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1318
1315
|
retry the request, or raise an exception, or return the request.
|
|
1319
1316
|
Default is a :py:class:`Handler`
|
|
1320
1317
|
|
|
1321
|
-
.. py:attribute::
|
|
1318
|
+
.. py:attribute:: mask_keys
|
|
1319
|
+
|
|
1320
|
+
Use this class attribute to declare keys to mask:
|
|
1322
1321
|
|
|
1323
|
-
|
|
1322
|
+
.. code-block:: python
|
|
1323
|
+
|
|
1324
|
+
class YourClient(cli2.Client):
|
|
1325
|
+
mask_keys = ['password', 'secret']
|
|
1324
1326
|
|
|
1325
1327
|
.. py:attribute:: cli
|
|
1326
1328
|
|
|
@@ -1350,6 +1352,10 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1350
1352
|
Enforce full logging: quiet requests are logged, masking does not
|
|
1351
1353
|
apply. This is also enabled with environment variable ``DEBUG``.
|
|
1352
1354
|
|
|
1355
|
+
.. py:attribute:: mask
|
|
1356
|
+
|
|
1357
|
+
:py:class:`~cli2.mask.Mask` object
|
|
1358
|
+
|
|
1353
1359
|
.. py:attribute:: models
|
|
1354
1360
|
|
|
1355
1361
|
Declared models for this Client.
|
|
@@ -1357,9 +1363,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1357
1363
|
paginator = Paginator
|
|
1358
1364
|
models = []
|
|
1359
1365
|
semaphore = None
|
|
1360
|
-
mask = []
|
|
1361
1366
|
debug = False
|
|
1362
1367
|
cmdclass = ClientCommand
|
|
1368
|
+
mask_keys = None
|
|
1363
1369
|
|
|
1364
1370
|
def __init__(self, *args, handler=None, semaphore=None, mask=None,
|
|
1365
1371
|
debug=False, **kwargs):
|
|
@@ -1373,8 +1379,12 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1373
1379
|
|
|
1374
1380
|
self.handler = handler or Handler()
|
|
1375
1381
|
self.semaphore = semaphore if semaphore else self.semaphore
|
|
1376
|
-
self.mask = mask
|
|
1382
|
+
self.mask = mask or Mask()
|
|
1383
|
+
if self.mask_keys:
|
|
1384
|
+
for key in self.mask_keys:
|
|
1385
|
+
self.mask.keys.add(key)
|
|
1377
1386
|
self.debug = debug or os.getenv('DEBUG', self.debug)
|
|
1387
|
+
self.mask.debug = self.debug
|
|
1378
1388
|
|
|
1379
1389
|
if truststore:
|
|
1380
1390
|
self._client_kwargs.setdefault(
|
|
@@ -1444,7 +1454,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1444
1454
|
def client(self):
|
|
1445
1455
|
self._client = None
|
|
1446
1456
|
|
|
1447
|
-
async def send(self, request, handler,
|
|
1457
|
+
async def send(self, request, handler, retries=True, semaphore=None,
|
|
1448
1458
|
log=None, auth=None, follow_redirects=None):
|
|
1449
1459
|
"""
|
|
1450
1460
|
Internal request method
|
|
@@ -1469,9 +1479,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1469
1479
|
try:
|
|
1470
1480
|
response = await _request()
|
|
1471
1481
|
except Exception as exc:
|
|
1472
|
-
await handler(self, exc, tries,
|
|
1482
|
+
await handler(self, exc, tries, log)
|
|
1473
1483
|
else:
|
|
1474
|
-
if response := await handler(self, response, tries,
|
|
1484
|
+
if response := await handler(self, response, tries, log):
|
|
1475
1485
|
return response
|
|
1476
1486
|
|
|
1477
1487
|
tries += 1
|
|
@@ -1605,7 +1615,6 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1605
1615
|
:param tries: Override for :py:attr:`Handler.tries`
|
|
1606
1616
|
:param backoff: Override for :py:attr:`Handler.backoff`
|
|
1607
1617
|
:param semaphore: Override for :py:attr:`Client.semaphore`
|
|
1608
|
-
:param mask: Override for :py:attr:`Client.mask`
|
|
1609
1618
|
"""
|
|
1610
1619
|
if not self.token and not self.token_getting:
|
|
1611
1620
|
await self.token_refresh()
|
|
@@ -1644,7 +1653,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1644
1653
|
|
|
1645
1654
|
_log = log.bind(method=method, url=str(request.url))
|
|
1646
1655
|
if not quiet or self.debug:
|
|
1647
|
-
key, value = self.request_log_data(request,
|
|
1656
|
+
key, value = self.request_log_data(request, quiet)
|
|
1648
1657
|
kwargs = dict()
|
|
1649
1658
|
if value:
|
|
1650
1659
|
kwargs[key] = value
|
|
@@ -1658,7 +1667,6 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1658
1667
|
handler=handler,
|
|
1659
1668
|
retries=retries,
|
|
1660
1669
|
semaphore=semaphore,
|
|
1661
|
-
mask=mask,
|
|
1662
1670
|
log=log,
|
|
1663
1671
|
auth=auth,
|
|
1664
1672
|
follow_redirects=follow_redirects,
|
|
@@ -1666,7 +1674,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1666
1674
|
|
|
1667
1675
|
kwargs = dict(status_code=response.status_code)
|
|
1668
1676
|
if not quiet or self.debug:
|
|
1669
|
-
key, value = self.response_log_data(response
|
|
1677
|
+
key, value = self.response_log_data(response)
|
|
1670
1678
|
if value:
|
|
1671
1679
|
kwargs[key] = value
|
|
1672
1680
|
|
|
@@ -1674,20 +1682,18 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1674
1682
|
|
|
1675
1683
|
return response
|
|
1676
1684
|
|
|
1677
|
-
def response_log_data(self, response
|
|
1678
|
-
mask = mask if mask is not None else self.mask
|
|
1685
|
+
def response_log_data(self, response):
|
|
1679
1686
|
try:
|
|
1680
1687
|
data = response.json()
|
|
1681
1688
|
except json.JSONDecodeError:
|
|
1682
1689
|
if response.content:
|
|
1683
|
-
return 'content', self.
|
|
1690
|
+
return 'content', self.mask(response.content)
|
|
1684
1691
|
else:
|
|
1685
1692
|
if data:
|
|
1686
|
-
return 'json', self.
|
|
1693
|
+
return 'json', self.mask(data)
|
|
1687
1694
|
return None, None
|
|
1688
1695
|
|
|
1689
|
-
def request_log_data(self, request,
|
|
1690
|
-
mask = mask if mask is not None else self.mask
|
|
1696
|
+
def request_log_data(self, request, quiet=False):
|
|
1691
1697
|
content = request.content.decode()
|
|
1692
1698
|
if not content:
|
|
1693
1699
|
return None, None
|
|
@@ -1697,7 +1703,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1697
1703
|
except json.JSONDecodeError:
|
|
1698
1704
|
pass
|
|
1699
1705
|
else:
|
|
1700
|
-
return 'json', self.
|
|
1706
|
+
return 'json', self.mask(data)
|
|
1701
1707
|
|
|
1702
1708
|
parsed = parse_qs(content)
|
|
1703
1709
|
if parsed:
|
|
@@ -1705,37 +1711,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1705
1711
|
key: value[0] if len(value) == 1 else value
|
|
1706
1712
|
for key, value in parsed.items()
|
|
1707
1713
|
}
|
|
1708
|
-
return 'data', self.
|
|
1714
|
+
return 'data', self.mask(data)
|
|
1709
1715
|
|
|
1710
|
-
return 'content', self.
|
|
1711
|
-
|
|
1712
|
-
def mask_content(self, content, mask=None):
|
|
1713
|
-
"""
|
|
1714
|
-
Implement content masking for non JSON content here.
|
|
1715
|
-
"""
|
|
1716
|
-
return content
|
|
1717
|
-
|
|
1718
|
-
def mask_data(self, data, mask=None):
|
|
1719
|
-
"""
|
|
1720
|
-
Apply mask for all :py:attr:`mask` in data.
|
|
1721
|
-
"""
|
|
1722
|
-
if self.debug:
|
|
1723
|
-
return data
|
|
1724
|
-
|
|
1725
|
-
mask = mask if mask is not None else self.mask
|
|
1726
|
-
|
|
1727
|
-
if isinstance(data, list):
|
|
1728
|
-
return [self.mask_data(item, mask) for item in data]
|
|
1729
|
-
|
|
1730
|
-
if isinstance(data, dict):
|
|
1731
|
-
for key, value in data.items():
|
|
1732
|
-
if key in mask:
|
|
1733
|
-
data[key] = '***MASKED***'
|
|
1734
|
-
if isinstance(value, dict):
|
|
1735
|
-
data[key] = self.mask_data(value, mask)
|
|
1736
|
-
if isinstance(value, list):
|
|
1737
|
-
data[key] = [self.mask_data(item, mask) for item in value]
|
|
1738
|
-
return data
|
|
1716
|
+
return 'content', self.mask(content)
|
|
1739
1717
|
|
|
1740
1718
|
return data
|
|
1741
1719
|
|
cli2-4.3.0/cli2/mask.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secret data masking module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LearnedError(Exception):
|
|
10
|
+
"""
|
|
11
|
+
Raised when a new value is learned, to run masking again.
|
|
12
|
+
"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Mask:
|
|
17
|
+
"""
|
|
18
|
+
Masking object that can learn values.
|
|
19
|
+
|
|
20
|
+
.. code-block:: python
|
|
21
|
+
|
|
22
|
+
mask = cli2.Mask(keys=['password'], values=['1337p4ssw0rD'])
|
|
23
|
+
result = mask(dict(password='xx', text='some 1337p4ssw0rD noise xx'))
|
|
24
|
+
|
|
25
|
+
Will cause result to be:
|
|
26
|
+
|
|
27
|
+
.. code-block:: yaml
|
|
28
|
+
|
|
29
|
+
password: ***MASKED***
|
|
30
|
+
text: some ***MASKED*** noise ***MASKED***
|
|
31
|
+
|
|
32
|
+
Because:
|
|
33
|
+
|
|
34
|
+
- ``1337p4ssw0rD`` was given as a value to mask
|
|
35
|
+
- ``xx`` was the value of a key named ``password`` which was given as a key
|
|
36
|
+
to mask
|
|
37
|
+
|
|
38
|
+
.. py:attribute:: keys
|
|
39
|
+
|
|
40
|
+
Set of keys that contain values to mask
|
|
41
|
+
|
|
42
|
+
.. py:attribute:: values
|
|
43
|
+
|
|
44
|
+
Set of values to mask
|
|
45
|
+
|
|
46
|
+
.. py:attribute:: renderer
|
|
47
|
+
|
|
48
|
+
Optionnal callback to render discovered values to mask
|
|
49
|
+
|
|
50
|
+
.. py:attribute:: debug
|
|
51
|
+
|
|
52
|
+
Enabled by the :envvar:`DEBUG` environment variable, makes this a no-op
|
|
53
|
+
(don't mask anything).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, keys=None, values=None, renderer=None, debug=False):
|
|
57
|
+
self.keys = set(keys) if keys else set()
|
|
58
|
+
self.values = set(values) if values else set()
|
|
59
|
+
self.renderer = renderer
|
|
60
|
+
if os.getenv('DEBUG'):
|
|
61
|
+
self.debug = True
|
|
62
|
+
else:
|
|
63
|
+
self.debug = debug
|
|
64
|
+
|
|
65
|
+
def __call__(self, data):
|
|
66
|
+
""""
|
|
67
|
+
Do our best to mask sensitive values in the data param recursively,
|
|
68
|
+
returning a masked copy of the passed data.
|
|
69
|
+
|
|
70
|
+
- when data is a dict: it is recursively iterated on, any value that in
|
|
71
|
+
is :py:attr:`keys` will have it's value replaced with
|
|
72
|
+
``***MASKED***``, also, the value is added to
|
|
73
|
+
:py:attr:`values`.
|
|
74
|
+
- when data is a string, each :py:attr:`values` will be replaced
|
|
75
|
+
with ``***MASKED***``, so we're actually able to mask sensitive
|
|
76
|
+
information from stdout outputs and the likes.
|
|
77
|
+
- when data is a list, each item is passed to :py:meth:`_mask()`.
|
|
78
|
+
|
|
79
|
+
Note that the :envvar:`DEBUG` environment variable will prevent any
|
|
80
|
+
masking at all.
|
|
81
|
+
|
|
82
|
+
:param data: Any kind of data to mask, will return a deepcopy of that.
|
|
83
|
+
"""
|
|
84
|
+
if self.debug:
|
|
85
|
+
return data
|
|
86
|
+
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
return self._mask(copy.deepcopy(data))
|
|
90
|
+
except LearnedError:
|
|
91
|
+
continue
|
|
92
|
+
else:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
def _mask(self, data):
|
|
96
|
+
"""
|
|
97
|
+
Actual, in-place masking method.
|
|
98
|
+
"""
|
|
99
|
+
if isinstance(data, dict):
|
|
100
|
+
for key, value in data.items():
|
|
101
|
+
if key in self.keys:
|
|
102
|
+
if self.renderer:
|
|
103
|
+
value = self.renderer(value)
|
|
104
|
+
if value not in self.values:
|
|
105
|
+
self.values.add(value)
|
|
106
|
+
raise LearnedError()
|
|
107
|
+
data[key] = '***MASKED***'
|
|
108
|
+
else:
|
|
109
|
+
data[key] = self._mask(value)
|
|
110
|
+
elif isinstance(data, list):
|
|
111
|
+
return [self._mask(item) for item in data]
|
|
112
|
+
elif isinstance(data, str):
|
|
113
|
+
for value in self.values:
|
|
114
|
+
data = data.replace(str(value), '***MASKED***')
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
def __repr__(self):
|
|
118
|
+
result = 'Mask(keys=[' + ', '.join(self.keys) + ']'
|
|
119
|
+
if self.values:
|
|
120
|
+
result += f', number_of_values={len(self.values)}'
|
|
121
|
+
return result + ')'
|
|
@@ -13,6 +13,7 @@ cli2/decorators.py
|
|
|
13
13
|
cli2/display.py
|
|
14
14
|
cli2/lock.py
|
|
15
15
|
cli2/log.py
|
|
16
|
+
cli2/mask.py
|
|
16
17
|
cli2/node.py
|
|
17
18
|
cli2/sphinx.py
|
|
18
19
|
cli2/table.py
|
|
@@ -49,6 +50,7 @@ tests/test_entry_point.py
|
|
|
49
50
|
tests/test_group.py
|
|
50
51
|
tests/test_inject.py
|
|
51
52
|
tests/test_lock.py
|
|
53
|
+
tests/test_mask.py
|
|
52
54
|
tests/test_node.py
|
|
53
55
|
tests/test_restful.py
|
|
54
56
|
tests/test_table.py
|
|
@@ -9,7 +9,7 @@ from cli2 import ansible
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ActionModule(ansible.ActionBase):
|
|
12
|
-
|
|
12
|
+
mask_keys = ['a']
|
|
13
13
|
|
|
14
14
|
async def run_async(self):
|
|
15
15
|
self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
|
|
@@ -19,7 +19,7 @@ class ActionModule(ansible.ActionBase):
|
|
|
19
19
|
async def test_mask(monkeypatch):
|
|
20
20
|
printer = mock.Mock()
|
|
21
21
|
monkeypatch.setattr(ActionModule, 'print', printer)
|
|
22
|
-
module = await ActionModule.run_test_async(facts=dict(
|
|
22
|
+
module = await ActionModule.run_test_async(facts=dict(mask_keys=['b']))
|
|
23
23
|
# result is untouched
|
|
24
24
|
assert module.result == {'x':
|
|
25
25
|
{'a': 'a', 'b': 'b', 'c': 'c', 'd': 'foo a rrr'}
|
|
@@ -32,7 +32,7 @@ async def test_mask(monkeypatch):
|
|
|
32
32
|
@pytest.mark.asyncio
|
|
33
33
|
async def test_response_error(httpx_mock):
|
|
34
34
|
class Client(cli2.Client):
|
|
35
|
-
|
|
35
|
+
mask_keys = ['secret']
|
|
36
36
|
|
|
37
37
|
class Action(ansible.ActionBase):
|
|
38
38
|
async def client_factory(self):
|
|
@@ -16,9 +16,9 @@ class HandlerSentinel(cli2.Handler):
|
|
|
16
16
|
super().__init__(*args, **kwargs)
|
|
17
17
|
self.calls = []
|
|
18
18
|
|
|
19
|
-
async def __call__(self, client, response, tries,
|
|
19
|
+
async def __call__(self, client, response, tries, log):
|
|
20
20
|
self.calls.append((client, response.status_code, tries))
|
|
21
|
-
return await super().__call__(client, response, tries,
|
|
21
|
+
return await super().__call__(client, response, tries, log)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@pytest.fixture
|
|
@@ -256,11 +256,11 @@ async def test_handler(client_class):
|
|
|
256
256
|
handler = cli2.Handler(accepts=[201], refuses=[218], retokens=[418])
|
|
257
257
|
|
|
258
258
|
response = httpx.Response(status_code=201)
|
|
259
|
-
result = await handler(client, response, 0,
|
|
259
|
+
result = await handler(client, response, 0, log)
|
|
260
260
|
assert result == response
|
|
261
261
|
|
|
262
262
|
response = httpx.Response(status_code=200)
|
|
263
|
-
result = await handler(client, response, 0,
|
|
263
|
+
result = await handler(client, response, 0, log)
|
|
264
264
|
log.info.assert_called_once_with(
|
|
265
265
|
'retry', status_code=200, tries=0, sleep=.0
|
|
266
266
|
)
|
|
@@ -269,7 +269,7 @@ async def test_handler(client_class):
|
|
|
269
269
|
response = httpx.Response(status_code=200, content='[2]')
|
|
270
270
|
response.request = httpx.Request('POST', '/', json=[1])
|
|
271
271
|
with pytest.raises(cli2.RetriesExceededError) as exc:
|
|
272
|
-
await handler(client, response, handler.tries + 1,
|
|
272
|
+
await handler(client, response, handler.tries + 1, log)
|
|
273
273
|
log.info.assert_called_once_with(
|
|
274
274
|
'retry', status_code=200, tries=0, sleep=.0
|
|
275
275
|
)
|
|
@@ -280,7 +280,7 @@ async def test_handler(client_class):
|
|
|
280
280
|
response = httpx.Response(status_code=200)
|
|
281
281
|
response.request = httpx.Request('GET', '/')
|
|
282
282
|
with pytest.raises(cli2.RetriesExceededError) as exc:
|
|
283
|
-
await handler(client, response, handler.tries + 1,
|
|
283
|
+
await handler(client, response, handler.tries + 1, log)
|
|
284
284
|
|
|
285
285
|
msg = 'Unacceptable response <Response [200 OK]> after 31 tries\n\x1b[0m\x1b[1mGET /\x1b[0m\n\x1b[1mHTTP 200\x1b[0m' # noqa
|
|
286
286
|
assert str(exc.value) == msg
|
|
@@ -288,7 +288,7 @@ async def test_handler(client_class):
|
|
|
288
288
|
response = httpx.Response(status_code=218)
|
|
289
289
|
response.request = httpx.Request('POST', '/')
|
|
290
290
|
with pytest.raises(cli2.RefusedResponseError) as exc:
|
|
291
|
-
await handler(client, response, 1,
|
|
291
|
+
await handler(client, response, 1, log)
|
|
292
292
|
|
|
293
293
|
assert exc.value.response
|
|
294
294
|
assert exc.value.request
|
|
@@ -299,12 +299,12 @@ async def test_handler(client_class):
|
|
|
299
299
|
response = httpx.Response(status_code=418)
|
|
300
300
|
response.request = httpx.Request('POST', '/')
|
|
301
301
|
with pytest.raises(cli2.TokenGetError):
|
|
302
|
-
await handler(client, response, 1,
|
|
302
|
+
await handler(client, response, 1, log)
|
|
303
303
|
|
|
304
304
|
assert not client.client_reset.await_count
|
|
305
305
|
exc = httpx.TransportError('foo')
|
|
306
306
|
exc.request = response.request
|
|
307
|
-
result = await handler(client, exc, 0,
|
|
307
|
+
result = await handler(client, exc, 0, log)
|
|
308
308
|
log.warn.assert_called_once_with(
|
|
309
309
|
'reconnect',
|
|
310
310
|
error="TransportError('foo')",
|
|
@@ -316,13 +316,13 @@ async def test_handler(client_class):
|
|
|
316
316
|
|
|
317
317
|
with pytest.raises(httpx.TransportError) as exc:
|
|
318
318
|
await handler(
|
|
319
|
-
client, httpx.TransportError('x'), handler.tries + 1,
|
|
319
|
+
client, httpx.TransportError('x'), handler.tries + 1, log
|
|
320
320
|
)
|
|
321
321
|
|
|
322
322
|
response = httpx.Response(status_code=418)
|
|
323
323
|
assert not client.token_reset.await_count
|
|
324
324
|
log.warn.reset_mock()
|
|
325
|
-
result = await handler(client, response, 0,
|
|
325
|
+
result = await handler(client, response, 0, log)
|
|
326
326
|
log.warn.assert_called_once_with(
|
|
327
327
|
'retoken', status_code=418, tries=0, sleep=.0
|
|
328
328
|
)
|
|
@@ -332,7 +332,7 @@ async def test_handler(client_class):
|
|
|
332
332
|
handler = cli2.Handler(accepts=[], refuses=[222])
|
|
333
333
|
|
|
334
334
|
response = httpx.Response(status_code=123)
|
|
335
|
-
result = await handler(client, response, 0,
|
|
335
|
+
result = await handler(client, response, 0, log)
|
|
336
336
|
assert result == response
|
|
337
337
|
|
|
338
338
|
|
|
@@ -867,7 +867,7 @@ def test_datetime_default_fmt(client_class):
|
|
|
867
867
|
|
|
868
868
|
@pytest.mark.asyncio
|
|
869
869
|
async def test_mask_recursive(client_class, monkeypatch):
|
|
870
|
-
client = client_class(mask=['scrt', 'password'])
|
|
870
|
+
client = client_class(mask=cli2.Mask(['scrt', 'password']))
|
|
871
871
|
client.client.send = mock.AsyncMock()
|
|
872
872
|
|
|
873
873
|
log = mock.Mock()
|
|
@@ -901,7 +901,7 @@ async def test_mask_recursive(client_class, monkeypatch):
|
|
|
901
901
|
'key', ('json', 'data'),
|
|
902
902
|
)
|
|
903
903
|
async def test_mask_logs(client_class, key, monkeypatch):
|
|
904
|
-
client = client_class(mask=['scrt', 'password'])
|
|
904
|
+
client = client_class(mask=cli2.Mask(['scrt', 'password']))
|
|
905
905
|
client.client.send = mock.AsyncMock()
|
|
906
906
|
|
|
907
907
|
log = mock.Mock()
|
|
@@ -933,27 +933,30 @@ async def test_mask_logs(client_class, key, monkeypatch):
|
|
|
933
933
|
@pytest.mark.asyncio
|
|
934
934
|
async def test_mask_exceptions(client_class):
|
|
935
935
|
class TestClient(client_class):
|
|
936
|
-
|
|
936
|
+
mask_keys = ['foo']
|
|
937
937
|
|
|
938
938
|
client = TestClient()
|
|
939
939
|
|
|
940
940
|
response = httpx.Response(status_code=218, content='{"c": 3, "d": 4}')
|
|
941
941
|
response.request = httpx.Request('POST', '/', json=dict(a=1, b=2))
|
|
942
|
-
|
|
942
|
+
client.mask.keys.add('a')
|
|
943
|
+
client.mask.keys.add('c')
|
|
944
|
+
error = cli2.ResponseError(client, response, 1)
|
|
943
945
|
expected = "\n\x1b[0m\x1b[1mPOST /\x1b[0m\n\x1b[94ma\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[94mb\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m2\x1b[37m\x1b[39;49;00m\n\n\x1b[1mHTTP 218\x1b[0m\n\x1b[94mc\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[94md\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m4\x1b[37m\x1b[39;49;00m\n" # noqa
|
|
944
946
|
assert str(error) == expected
|
|
945
947
|
|
|
946
948
|
# this needs to work with form data too
|
|
947
949
|
response = httpx.Response(status_code=218, content='{"c": 3, "d": 4}')
|
|
948
950
|
response.request = httpx.Request('POST', '/', data=dict(a=1, b=2))
|
|
949
|
-
|
|
951
|
+
client.mask = cli2.Mask(['a', 'c'])
|
|
952
|
+
error = cli2.ResponseError(client, response, 1)
|
|
950
953
|
expected = "\n\x1b[0m\x1b[1mPOST /\x1b[0m\n\x1b[94ma\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[94mb\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m2\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\n\x1b[1mHTTP 218\x1b[0m\n\x1b[94mc\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[94md\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m4\x1b[37m\x1b[39;49;00m\n" # noqa
|
|
951
954
|
assert str(error) == expected
|
|
952
955
|
|
|
953
956
|
|
|
954
957
|
@pytest.mark.asyncio
|
|
955
958
|
async def test_request_mask(client_class, monkeypatch):
|
|
956
|
-
client = client_class(mask=['password'])
|
|
959
|
+
client = client_class(mask=cli2.Mask(['password']))
|
|
957
960
|
client.client.send = mock.AsyncMock()
|
|
958
961
|
|
|
959
962
|
log = mock.Mock()
|
|
@@ -965,7 +968,8 @@ async def test_request_mask(client_class, monkeypatch):
|
|
|
965
968
|
data = dict(foo='bar', password='secret')
|
|
966
969
|
response.request = httpx.Request('POST', '/', json=data)
|
|
967
970
|
client.client.send.return_value = response
|
|
968
|
-
|
|
971
|
+
client.mask.keys.add('scrt')
|
|
972
|
+
await client.post('/', json=data)
|
|
969
973
|
log.bind.assert_called_once_with(
|
|
970
974
|
method='POST',
|
|
971
975
|
url='http://lol/'
|
|
@@ -973,7 +977,7 @@ async def test_request_mask(client_class, monkeypatch):
|
|
|
973
977
|
log = log.bind.return_value
|
|
974
978
|
log.debug.assert_called_once_with(
|
|
975
979
|
'request',
|
|
976
|
-
json=dict(foo='bar', password='
|
|
980
|
+
json=dict(foo='bar', password='***MASKED***'),
|
|
977
981
|
)
|
|
978
982
|
log.info.assert_called_once_with(
|
|
979
983
|
'response',
|
|
@@ -1025,11 +1029,11 @@ async def test_log_quiet(client_class, monkeypatch):
|
|
|
1025
1029
|
def test_class_override(client_class):
|
|
1026
1030
|
class TestClient(client_class):
|
|
1027
1031
|
semaphore = 'foo'
|
|
1028
|
-
|
|
1032
|
+
mask_keys = ['bar']
|
|
1029
1033
|
debug = True
|
|
1030
1034
|
|
|
1031
1035
|
assert TestClient().semaphore == 'foo'
|
|
1032
|
-
assert TestClient().mask == 'bar'
|
|
1036
|
+
assert TestClient().mask.keys == {'bar'}
|
|
1033
1037
|
assert TestClient().debug
|
|
1034
1038
|
|
|
1035
1039
|
|
|
@@ -1081,7 +1085,7 @@ def test_id_value(client_class):
|
|
|
1081
1085
|
|
|
1082
1086
|
@pytest.mark.asyncio
|
|
1083
1087
|
async def test_debug(client_class, monkeypatch):
|
|
1084
|
-
client = client_class(mask=['scrt', 'password'], debug=True)
|
|
1088
|
+
client = client_class(mask=cli2.Mask(['scrt', 'password']), debug=True)
|
|
1085
1089
|
client.client.send = mock.AsyncMock()
|
|
1086
1090
|
|
|
1087
1091
|
log = mock.Mock()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import cli2
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_mask():
|
|
6
|
+
mask = cli2.Mask(['seckey'], ['secval'])
|
|
7
|
+
fixture = {
|
|
8
|
+
'a': {
|
|
9
|
+
'b': 'secval b secret',
|
|
10
|
+
'seckey': 'secret',
|
|
11
|
+
},
|
|
12
|
+
'b': ['secval foo secret'],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
expected = {
|
|
16
|
+
'a': {
|
|
17
|
+
'b': '***MASKED*** b ***MASKED***',
|
|
18
|
+
'seckey': '***MASKED***',
|
|
19
|
+
},
|
|
20
|
+
'b': ['***MASKED*** foo ***MASKED***']
|
|
21
|
+
}
|
|
22
|
+
assert mask(fixture) == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|