cli2 4.2.10__tar.gz → 4.3.1__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.10/cli2.egg-info → cli2-4.3.1}/PKG-INFO +1 -1
- {cli2-4.2.10 → cli2-4.3.1}/cli2/__init__.py +1 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/ansible/action.py +50 -80
- {cli2-4.2.10 → cli2-4.3.1}/cli2/client.py +41 -62
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/client.py +1 -1
- cli2-4.3.1/cli2/mask.py +124 -0
- {cli2-4.2.10 → cli2-4.3.1/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-4.2.10 → cli2-4.3.1}/cli2.egg-info/SOURCES.txt +2 -0
- {cli2-4.2.10 → cli2-4.3.1}/setup.py +1 -1
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_ansible.py +3 -3
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_client.py +32 -23
- cli2-4.3.1/tests/test_mask.py +25 -0
- {cli2-4.2.10 → cli2-4.3.1}/MANIFEST.in +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/README.rst +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/classifiers.txt +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/ansible/__init__.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/ansible/playbook.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/ansible/variables.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/asyncio.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/cli.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/cli2.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/colors.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/configuration.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/decorators.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/display.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/__init__.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/conf.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/example.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/example_obj.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/nesting.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/obj.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/obj2.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/examples/test.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/lock.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/log.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/node.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/sphinx.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/table.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2/test.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2.egg-info/requires.txt +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/setup.cfg +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_ansible_variables.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_asyncio.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_cli.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_command.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_configuration.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_decorators.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_display.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_entry_point.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_group.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_inject.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_lock.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_node.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/tests/test_restful.py +0 -0
- {cli2-4.2.10 → cli2-4.3.1}/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,18 +166,27 @@ 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)
|
|
189
|
+
self.mask_initial_values = self.mask.values
|
|
196
190
|
|
|
197
191
|
async def run_wrapped_async(self):
|
|
198
192
|
self.verbosity = self.task_vars.get('ansible_verbosity', 0)
|
|
@@ -209,7 +203,10 @@ class ActionBase(ActionBase):
|
|
|
209
203
|
self.client = await self.client_factory()
|
|
210
204
|
except NotImplementedError:
|
|
211
205
|
self.client = None
|
|
212
|
-
|
|
206
|
+
self.mask = cli2.Mask()
|
|
207
|
+
else:
|
|
208
|
+
self.mask = copy.deepcopy(self.client.mask)
|
|
209
|
+
self.mask_init()
|
|
213
210
|
await self.run_async()
|
|
214
211
|
except Exception as exc:
|
|
215
212
|
self.result['failed'] = True
|
|
@@ -236,18 +233,27 @@ class ActionBase(ActionBase):
|
|
|
236
233
|
self.exc = exc
|
|
237
234
|
finally:
|
|
238
235
|
if self.mask and self.verbosity:
|
|
236
|
+
# this task has a mask, so it's probably a no_log, we're
|
|
237
|
+
# running verbose, print masked result
|
|
239
238
|
self.print_yaml(self.result)
|
|
240
239
|
|
|
240
|
+
if self.mask_initial_values != self.mask.values:
|
|
241
|
+
# this module has discovered new values to mask, update the
|
|
242
|
+
# fact with those values
|
|
243
|
+
self.result['ansible_facts'] = dict(
|
|
244
|
+
mask_values=self.mask.values,
|
|
245
|
+
)
|
|
246
|
+
|
|
241
247
|
if (
|
|
242
248
|
self._before_data != UNSET_DEFAULT
|
|
243
249
|
and self._after_data != UNSET_DEFAULT
|
|
244
250
|
):
|
|
245
251
|
diff = difflib.unified_diff(
|
|
246
252
|
to_nice_yaml(
|
|
247
|
-
self.
|
|
253
|
+
self.mask(self._before_data)
|
|
248
254
|
).splitlines(),
|
|
249
255
|
to_nice_yaml(
|
|
250
|
-
self.
|
|
256
|
+
self.mask(self._after_data)
|
|
251
257
|
).splitlines(),
|
|
252
258
|
self._before_label,
|
|
253
259
|
self._after_label,
|
|
@@ -258,7 +264,7 @@ class ActionBase(ActionBase):
|
|
|
258
264
|
"""
|
|
259
265
|
Render data as masked yaml.
|
|
260
266
|
"""
|
|
261
|
-
data = self.
|
|
267
|
+
data = self.mask(data)
|
|
262
268
|
yaml = to_nice_yaml(data)
|
|
263
269
|
rendered = cli2.yaml_highlight(yaml)
|
|
264
270
|
self.print(rendered)
|
|
@@ -267,42 +273,6 @@ class ActionBase(ActionBase):
|
|
|
267
273
|
# this serves for mocking
|
|
268
274
|
print(data)
|
|
269
275
|
|
|
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
276
|
async def run_async(self):
|
|
307
277
|
"""
|
|
308
278
|
The method you are supposed to implement.
|
|
@@ -406,7 +376,7 @@ class ActionBase(ActionBase):
|
|
|
406
376
|
if self.verbosity:
|
|
407
377
|
display.display(
|
|
408
378
|
f'<{self.task_vars["inventory_hostname"]}> + '
|
|
409
|
-
+ self.
|
|
379
|
+
+ self.mask(cmd),
|
|
410
380
|
color='blue',
|
|
411
381
|
)
|
|
412
382
|
shell_action = self._shared_loader_obj.action_loader.get(
|
|
@@ -422,9 +392,9 @@ class ActionBase(ActionBase):
|
|
|
422
392
|
|
|
423
393
|
if self.verbosity:
|
|
424
394
|
if 'stderr_lines' in result:
|
|
425
|
-
print(self.
|
|
395
|
+
self.print(self.mask(result['stderr']))
|
|
426
396
|
if 'stdout_lines' in result:
|
|
427
|
-
print(self.
|
|
397
|
+
self.print(self.mask(result['stdout']))
|
|
428
398
|
|
|
429
399
|
result.pop('invocation')
|
|
430
400
|
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:
|
|
@@ -697,7 +698,8 @@ class ModelCommand(Command):
|
|
|
697
698
|
return await model.get(id=self['id'].value)
|
|
698
699
|
|
|
699
700
|
async def post_call(self):
|
|
700
|
-
|
|
701
|
+
if self.client:
|
|
702
|
+
await self.client.post_call(self)
|
|
701
703
|
|
|
702
704
|
|
|
703
705
|
class ModelMetaclass(type):
|
|
@@ -850,7 +852,7 @@ class Model(metaclass=ModelMetaclass):
|
|
|
850
852
|
|
|
851
853
|
@property
|
|
852
854
|
def data_masked(self):
|
|
853
|
-
return self.client.
|
|
855
|
+
return self.client.mask(self.data)
|
|
854
856
|
|
|
855
857
|
@classmethod
|
|
856
858
|
@hide('expressions')
|
|
@@ -1098,7 +1100,7 @@ class Handler:
|
|
|
1098
1100
|
self.tries = self.tries_default if tries is None else tries
|
|
1099
1101
|
self.backoff = self.backoff_default if backoff is None else backoff
|
|
1100
1102
|
|
|
1101
|
-
async def __call__(self, client, response, tries,
|
|
1103
|
+
async def __call__(self, client, response, tries, log):
|
|
1102
1104
|
seconds = tries * self.backoff
|
|
1103
1105
|
|
|
1104
1106
|
if isinstance(response, Exception):
|
|
@@ -1126,24 +1128,24 @@ class Handler:
|
|
|
1126
1128
|
return response
|
|
1127
1129
|
|
|
1128
1130
|
if response.status_code in self.refuses:
|
|
1129
|
-
raise RefusedResponseError(client, response, tries
|
|
1131
|
+
raise RefusedResponseError(client, response, tries)
|
|
1130
1132
|
|
|
1131
1133
|
if tries >= self.tries:
|
|
1132
|
-
raise RetriesExceededError(client, response, tries
|
|
1134
|
+
raise RetriesExceededError(client, response, tries)
|
|
1133
1135
|
|
|
1134
1136
|
kwargs = dict(
|
|
1135
1137
|
status_code=response.status_code,
|
|
1136
1138
|
tries=tries,
|
|
1137
1139
|
sleep=seconds,
|
|
1138
1140
|
)
|
|
1139
|
-
key, value = client.response_log_data(response
|
|
1141
|
+
key, value = client.response_log_data(response)
|
|
1140
1142
|
if value:
|
|
1141
1143
|
kwargs[key] = value
|
|
1142
1144
|
|
|
1143
1145
|
if response.status_code in self.retokens:
|
|
1144
1146
|
if tries:
|
|
1145
1147
|
# our authentication is just not working, no need to retry
|
|
1146
|
-
raise TokenGetError(client, response, tries
|
|
1148
|
+
raise TokenGetError(client, response, tries)
|
|
1147
1149
|
log.warn('retoken', **kwargs)
|
|
1148
1150
|
await client.token_reset()
|
|
1149
1151
|
|
|
@@ -1179,11 +1181,10 @@ class ResponseError(ClientError):
|
|
|
1179
1181
|
|
|
1180
1182
|
Request method
|
|
1181
1183
|
"""
|
|
1182
|
-
def __init__(self, client, response, tries,
|
|
1184
|
+
def __init__(self, client, response, tries, msg=None):
|
|
1183
1185
|
self.client = client
|
|
1184
1186
|
self.response = response
|
|
1185
1187
|
self.tries = tries
|
|
1186
|
-
self.mask = mask
|
|
1187
1188
|
self.msg = msg or getattr(self, 'msg', '').format(self=self)
|
|
1188
1189
|
super().__init__(self.enhance(self.msg))
|
|
1189
1190
|
|
|
@@ -1212,10 +1213,7 @@ class ResponseError(ClientError):
|
|
|
1212
1213
|
:param exc: httpx.HTTPStatusError
|
|
1213
1214
|
"""
|
|
1214
1215
|
output = [msg]
|
|
1215
|
-
key, value = self.client.request_log_data(
|
|
1216
|
-
self.response.request,
|
|
1217
|
-
self.mask,
|
|
1218
|
-
)
|
|
1216
|
+
key, value = self.client.request_log_data(self.response.request)
|
|
1219
1217
|
request_msg = ' '.join([
|
|
1220
1218
|
str(self.response.request.method),
|
|
1221
1219
|
str(self.response.request.url),
|
|
@@ -1227,7 +1225,7 @@ class ResponseError(ClientError):
|
|
|
1227
1225
|
if value:
|
|
1228
1226
|
output.append(display.render(value))
|
|
1229
1227
|
|
|
1230
|
-
key, value = self.client.response_log_data(self.response
|
|
1228
|
+
key, value = self.client.response_log_data(self.response)
|
|
1231
1229
|
output.append(
|
|
1232
1230
|
''.join([
|
|
1233
1231
|
colors.bold,
|
|
@@ -1318,9 +1316,14 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1318
1316
|
retry the request, or raise an exception, or return the request.
|
|
1319
1317
|
Default is a :py:class:`Handler`
|
|
1320
1318
|
|
|
1321
|
-
.. py:attribute::
|
|
1319
|
+
.. py:attribute:: mask_keys
|
|
1322
1320
|
|
|
1323
|
-
|
|
1321
|
+
Use this class attribute to declare keys to mask:
|
|
1322
|
+
|
|
1323
|
+
.. code-block:: python
|
|
1324
|
+
|
|
1325
|
+
class YourClient(cli2.Client):
|
|
1326
|
+
mask_keys = ['password', 'secret']
|
|
1324
1327
|
|
|
1325
1328
|
.. py:attribute:: cli
|
|
1326
1329
|
|
|
@@ -1350,6 +1353,10 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1350
1353
|
Enforce full logging: quiet requests are logged, masking does not
|
|
1351
1354
|
apply. This is also enabled with environment variable ``DEBUG``.
|
|
1352
1355
|
|
|
1356
|
+
.. py:attribute:: mask
|
|
1357
|
+
|
|
1358
|
+
:py:class:`~cli2.mask.Mask` object
|
|
1359
|
+
|
|
1353
1360
|
.. py:attribute:: models
|
|
1354
1361
|
|
|
1355
1362
|
Declared models for this Client.
|
|
@@ -1357,9 +1364,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1357
1364
|
paginator = Paginator
|
|
1358
1365
|
models = []
|
|
1359
1366
|
semaphore = None
|
|
1360
|
-
mask = []
|
|
1361
1367
|
debug = False
|
|
1362
1368
|
cmdclass = ClientCommand
|
|
1369
|
+
mask_keys = None
|
|
1363
1370
|
|
|
1364
1371
|
def __init__(self, *args, handler=None, semaphore=None, mask=None,
|
|
1365
1372
|
debug=False, **kwargs):
|
|
@@ -1373,8 +1380,12 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1373
1380
|
|
|
1374
1381
|
self.handler = handler or Handler()
|
|
1375
1382
|
self.semaphore = semaphore if semaphore else self.semaphore
|
|
1376
|
-
self.mask = mask
|
|
1383
|
+
self.mask = mask or Mask()
|
|
1384
|
+
if self.mask_keys:
|
|
1385
|
+
for key in self.mask_keys:
|
|
1386
|
+
self.mask.keys.add(key)
|
|
1377
1387
|
self.debug = debug or os.getenv('DEBUG', self.debug)
|
|
1388
|
+
self.mask.debug = self.debug
|
|
1378
1389
|
|
|
1379
1390
|
if truststore:
|
|
1380
1391
|
self._client_kwargs.setdefault(
|
|
@@ -1444,7 +1455,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1444
1455
|
def client(self):
|
|
1445
1456
|
self._client = None
|
|
1446
1457
|
|
|
1447
|
-
async def send(self, request, handler,
|
|
1458
|
+
async def send(self, request, handler, retries=True, semaphore=None,
|
|
1448
1459
|
log=None, auth=None, follow_redirects=None):
|
|
1449
1460
|
"""
|
|
1450
1461
|
Internal request method
|
|
@@ -1469,9 +1480,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1469
1480
|
try:
|
|
1470
1481
|
response = await _request()
|
|
1471
1482
|
except Exception as exc:
|
|
1472
|
-
await handler(self, exc, tries,
|
|
1483
|
+
await handler(self, exc, tries, log)
|
|
1473
1484
|
else:
|
|
1474
|
-
if response := await handler(self, response, tries,
|
|
1485
|
+
if response := await handler(self, response, tries, log):
|
|
1475
1486
|
return response
|
|
1476
1487
|
|
|
1477
1488
|
tries += 1
|
|
@@ -1605,7 +1616,6 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1605
1616
|
:param tries: Override for :py:attr:`Handler.tries`
|
|
1606
1617
|
:param backoff: Override for :py:attr:`Handler.backoff`
|
|
1607
1618
|
:param semaphore: Override for :py:attr:`Client.semaphore`
|
|
1608
|
-
:param mask: Override for :py:attr:`Client.mask`
|
|
1609
1619
|
"""
|
|
1610
1620
|
if not self.token and not self.token_getting:
|
|
1611
1621
|
await self.token_refresh()
|
|
@@ -1644,7 +1654,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1644
1654
|
|
|
1645
1655
|
_log = log.bind(method=method, url=str(request.url))
|
|
1646
1656
|
if not quiet or self.debug:
|
|
1647
|
-
key, value = self.request_log_data(request,
|
|
1657
|
+
key, value = self.request_log_data(request, quiet)
|
|
1648
1658
|
kwargs = dict()
|
|
1649
1659
|
if value:
|
|
1650
1660
|
kwargs[key] = value
|
|
@@ -1658,7 +1668,6 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1658
1668
|
handler=handler,
|
|
1659
1669
|
retries=retries,
|
|
1660
1670
|
semaphore=semaphore,
|
|
1661
|
-
mask=mask,
|
|
1662
1671
|
log=log,
|
|
1663
1672
|
auth=auth,
|
|
1664
1673
|
follow_redirects=follow_redirects,
|
|
@@ -1666,7 +1675,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1666
1675
|
|
|
1667
1676
|
kwargs = dict(status_code=response.status_code)
|
|
1668
1677
|
if not quiet or self.debug:
|
|
1669
|
-
key, value = self.response_log_data(response
|
|
1678
|
+
key, value = self.response_log_data(response)
|
|
1670
1679
|
if value:
|
|
1671
1680
|
kwargs[key] = value
|
|
1672
1681
|
|
|
@@ -1674,20 +1683,18 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1674
1683
|
|
|
1675
1684
|
return response
|
|
1676
1685
|
|
|
1677
|
-
def response_log_data(self, response
|
|
1678
|
-
mask = mask if mask is not None else self.mask
|
|
1686
|
+
def response_log_data(self, response):
|
|
1679
1687
|
try:
|
|
1680
1688
|
data = response.json()
|
|
1681
1689
|
except json.JSONDecodeError:
|
|
1682
1690
|
if response.content:
|
|
1683
|
-
return 'content', self.
|
|
1691
|
+
return 'content', self.mask(response.content)
|
|
1684
1692
|
else:
|
|
1685
1693
|
if data:
|
|
1686
|
-
return 'json', self.
|
|
1694
|
+
return 'json', self.mask(data)
|
|
1687
1695
|
return None, None
|
|
1688
1696
|
|
|
1689
|
-
def request_log_data(self, request,
|
|
1690
|
-
mask = mask if mask is not None else self.mask
|
|
1697
|
+
def request_log_data(self, request, quiet=False):
|
|
1691
1698
|
content = request.content.decode()
|
|
1692
1699
|
if not content:
|
|
1693
1700
|
return None, None
|
|
@@ -1697,7 +1704,7 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1697
1704
|
except json.JSONDecodeError:
|
|
1698
1705
|
pass
|
|
1699
1706
|
else:
|
|
1700
|
-
return 'json', self.
|
|
1707
|
+
return 'json', self.mask(data)
|
|
1701
1708
|
|
|
1702
1709
|
parsed = parse_qs(content)
|
|
1703
1710
|
if parsed:
|
|
@@ -1705,37 +1712,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1705
1712
|
key: value[0] if len(value) == 1 else value
|
|
1706
1713
|
for key, value in parsed.items()
|
|
1707
1714
|
}
|
|
1708
|
-
return 'data', self.
|
|
1709
|
-
|
|
1710
|
-
return 'content', self.mask_content(content, mask)
|
|
1715
|
+
return 'data', self.mask(data)
|
|
1711
1716
|
|
|
1712
|
-
|
|
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
|
|
1717
|
+
return 'content', self.mask(content)
|
|
1739
1718
|
|
|
1740
1719
|
return data
|
|
1741
1720
|
|
cli2-4.3.1/cli2/mask.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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 + ')'
|
|
122
|
+
|
|
123
|
+
def __bool__(self):
|
|
124
|
+
return bool(self.keys or self.values)
|
|
@@ -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()
|
|
@@ -1191,3 +1195,8 @@ def test_client_command(client_class, httpx_mock):
|
|
|
1191
1195
|
assert Client.cli['model']['get'].client is None
|
|
1192
1196
|
cmd = Client.cli['model']['find']
|
|
1193
1197
|
cmd('http://x')
|
|
1198
|
+
|
|
1199
|
+
Client.post_call_called = False
|
|
1200
|
+
cmd = Client.cli['model']['get']
|
|
1201
|
+
cmd()
|
|
1202
|
+
assert not Client.post_call_called
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
23
|
+
|
|
24
|
+
assert mask
|
|
25
|
+
assert not cli2.Mask()
|
|
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
|