summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-10-29 17:13:25 +0000
committerGerrit Code Review <review@openstack.org>2018-10-29 17:13:25 +0000
commit56e606bf4baa52b956f0b93032692cd4943b7341 (patch)
treeba95a04730bc093dc2f3aab21a496bb75598f587
parent475655ac5a71dfb29876a68c3f8a71208df35b18 (diff)
parent47ade1f0da6b4e2e6d3dafccfd88740be910bea7 (diff)
Merge "fix: Redact secondhand substitutions of sensitive data"
-rw-r--r--deckhand/common/document.py5
-rw-r--r--deckhand/common/utils.py28
-rw-r--r--deckhand/control/common.py15
-rw-r--r--deckhand/engine/layering.py9
-rw-r--r--deckhand/engine/render.py8
-rw-r--r--deckhand/engine/secrets_manager.py114
-rw-r--r--deckhand/tests/unit/control/test_rendered_documents_controller.py26
-rw-r--r--deckhand/tests/unit/engine/test_secrets_manager.py6
8 files changed, 137 insertions, 74 deletions
diff --git a/deckhand/common/document.py b/deckhand/common/document.py
index c4d8f99..739cfc3 100644
--- a/deckhand/common/document.py
+++ b/deckhand/common/document.py
@@ -173,9 +173,8 @@ class DocumentDict(dict):
173 return [DocumentDict(d) for d in documents] 173 return [DocumentDict(d) for d in documents]
174 174
175 @classmethod 175 @classmethod
176 def redact(cls, input): 176 def redact(cls, field):
177 return hashlib.sha256(json.dumps(input) 177 return hashlib.sha256(json.dumps(field).encode('utf-8')).hexdigest()
178 .encode('utf-8')).hexdigest()
179 178
180 179
181def document_dict_representer(dumper, data): 180def document_dict_representer(dumper, data):
diff --git a/deckhand/common/utils.py b/deckhand/common/utils.py
index 26473e0..2645e43 100644
--- a/deckhand/common/utils.py
+++ b/deckhand/common/utils.py
@@ -396,23 +396,23 @@ def redact_document(document):
396 """ 396 """
397 d = _to_document(document) 397 d = _to_document(document)
398 if d.is_encrypted: 398 if d.is_encrypted:
399 document['data'] = document_dict.redact(d.data) 399 d.data = document_dict.redact(d.data)
400 # FIXME(felipemonteiro): This block should be out-dented by 4 spaces 400 for s in d.substitutions:
401 # because cleartext documents that substitute from encrypted documents 401 s['src']['path'] = document_dict.redact(s['src']['path'])
402 # should be subject to this redaction as well. However, doing this 402 s['dest']['path'] = document_dict.redact(s['dest']['path'])
403 # will result in substitution failures; the solution is to add a 403 return d
404 # helper to :class:`deckhand.common.DocumentDict` that checks whether
405 # its metadata.substitutions is redacted - if so, skips substitution.
406 if d.substitutions:
407 subs = d.substitutions
408 for s in subs:
409 s['src']['path'] = document_dict.redact(s['src']['path'])
410 s['dest']['path'] = document_dict.redact(s['dest']['path'])
411 document['metadata']['substitutions'] = subs
412 return document
413 404
414 405
415def redact_documents(documents): 406def redact_documents(documents):
407 """Redact sensitive data for each document in ``documents``.
408
409 Sensitive data includes ``data``, ``substitutions[n].src.path``, and
410 ``substitutions[n].dest.path`` fields.
411
412 :param list[dict] documents: List of documents whose data to redact.
413 :returns: Documents with redacted sensitive data.
414 :rtype: list[dict]
415 """
416 return [redact_document(d) for d in documents] 416 return [redact_document(d) for d in documents]
417 417
418 418
diff --git a/deckhand/control/common.py b/deckhand/control/common.py
index 73727e2..b741f20 100644
--- a/deckhand/control/common.py
+++ b/deckhand/control/common.py
@@ -148,6 +148,18 @@ def invalidate_cache_data():
148 148
149 149
150def get_rendered_docs(revision_id, cleartext_secrets=False, **filters): 150def get_rendered_docs(revision_id, cleartext_secrets=False, **filters):
151 """Helper for retrieving rendered documents for ``revision_id``.
152
153 Retrieves raw documents from DB, renders them, and returns rendered result
154 set.
155
156 :param int revision_id: Revision ID whose documents to render.
157 :param bool cleartext_secrets: Whether to show unencrypted data as
158 cleartext.
159 :param filters: Filters used for retrieving raw documents from DB.
160 :returns: List of rendered documents.
161 :rtype: list[dict]
162 """
151 data = _retrieve_documents_for_rendering(revision_id, **filters) 163 data = _retrieve_documents_for_rendering(revision_id, **filters)
152 documents = document_wrapper.DocumentDict.from_list(data) 164 documents = document_wrapper.DocumentDict.from_list(data)
153 encryption_sources = _resolve_encrypted_data(documents) 165 encryption_sources = _resolve_encrypted_data(documents)
@@ -158,7 +170,8 @@ def get_rendered_docs(revision_id, cleartext_secrets=False, **filters):
158 return engine.render( 170 return engine.render(
159 revision_id, 171 revision_id,
160 documents, 172 documents,
161 encryption_sources=encryption_sources) 173 encryption_sources=encryption_sources,
174 cleartext_secrets=cleartext_secrets)
162 except (errors.BarbicanClientException, 175 except (errors.BarbicanClientException,
163 errors.BarbicanServerException, 176 errors.BarbicanServerException,
164 errors.InvalidDocumentLayer, 177 errors.InvalidDocumentLayer,
diff --git a/deckhand/engine/layering.py b/deckhand/engine/layering.py
index 952a31a..aaa3b45 100644
--- a/deckhand/engine/layering.py
+++ b/deckhand/engine/layering.py
@@ -384,7 +384,8 @@ class DocumentLayering(object):
384 documents, 384 documents,
385 validate=True, 385 validate=True,
386 fail_on_missing_sub_src=True, 386 fail_on_missing_sub_src=True,
387 encryption_sources=None): 387 encryption_sources=None,
388 cleartext_secrets=False):
388 """Contructor for ``DocumentLayering``. 389 """Contructor for ``DocumentLayering``.
389 390
390 :param layering_policy: The document with schema 391 :param layering_policy: The document with schema
@@ -405,6 +406,9 @@ class DocumentLayering(object):
405 actual unecrypted data. If encrypting data with Barbican, the 406 actual unecrypted data. If encrypting data with Barbican, the
406 reference will be a Barbican secret reference. 407 reference will be a Barbican secret reference.
407 :type encryption_sources: dict 408 :type encryption_sources: dict
409 :param cleartext_secrets: Whether to show unencrypted data as
410 cleartext.
411 :type cleartext_secrets: bool
408 412
409 :raises LayeringPolicyNotFound: If no LayeringPolicy was found among 413 :raises LayeringPolicyNotFound: If no LayeringPolicy was found among
410 list of ``documents``. 414 list of ``documents``.
@@ -482,7 +486,8 @@ class DocumentLayering(object):
482 self.secrets_substitution = secrets_manager.SecretsSubstitution( 486 self.secrets_substitution = secrets_manager.SecretsSubstitution(
483 substitution_sources, 487 substitution_sources,
484 encryption_sources=encryption_sources, 488 encryption_sources=encryption_sources,
485 fail_on_missing_sub_src=fail_on_missing_sub_src) 489 fail_on_missing_sub_src=fail_on_missing_sub_src,
490 cleartext_secrets=cleartext_secrets)
486 491
487 self._sorted_documents = self._topologically_sort_documents( 492 self._sorted_documents = self._topologically_sort_documents(
488 substitution_sources) 493 substitution_sources)
diff --git a/deckhand/engine/render.py b/deckhand/engine/render.py
index 86f7548..8d71482 100644
--- a/deckhand/engine/render.py
+++ b/deckhand/engine/render.py
@@ -24,7 +24,8 @@ __all__ = ('render',
24 'validate_render') 24 'validate_render')
25 25
26 26
27def render(revision_id, documents, encryption_sources=None): 27def render(revision_id, documents, encryption_sources=None,
28 cleartext_secrets=False):
28 """Render revision documents for ``revision_id`` using raw ``documents``. 29 """Render revision documents for ``revision_id`` using raw ``documents``.
29 30
30 :param revision_id: Key used for caching rendered documents by. 31 :param revision_id: Key used for caching rendered documents by.
@@ -37,6 +38,8 @@ def render(revision_id, documents, encryption_sources=None):
37 actual unecrypted data. If encrypting data with Barbican, the 38 actual unecrypted data. If encrypting data with Barbican, the
38 reference will be a Barbican secret reference. 39 reference will be a Barbican secret reference.
39 :type encryption_sources: dict 40 :type encryption_sources: dict
41 :param cleartext_secrets: Whether to show unencrypted data as cleartext.
42 :type cleartext_secrets: bool
40 :returns: Rendered documents for ``revision_id``. 43 :returns: Rendered documents for ``revision_id``.
41 :rtype: List[dict] 44 :rtype: List[dict]
42 45
@@ -49,7 +52,8 @@ def render(revision_id, documents, encryption_sources=None):
49 revision_id, 52 revision_id,
50 documents, 53 documents,
51 encryption_sources=encryption_sources, 54 encryption_sources=encryption_sources,
52 validate=False) 55 validate=False,
56 cleartext_secrets=cleartext_secrets)
53 57
54 58
55def validate_render(revision_id, rendered_documents, validator): 59def validate_render(revision_id, rendered_documents, validator):
diff --git a/deckhand/engine/secrets_manager.py b/deckhand/engine/secrets_manager.py
index 3a72631..fb52a4c 100644
--- a/deckhand/engine/secrets_manager.py
+++ b/deckhand/engine/secrets_manager.py
@@ -20,7 +20,7 @@ from oslo_log import log as logging
20import six 20import six
21 21
22from deckhand.barbican.driver import BarbicanDriver 22from deckhand.barbican.driver import BarbicanDriver
23from deckhand.common import document as document_wrapper 23from deckhand.common.document import DocumentDict as dd
24from deckhand.common import utils 24from deckhand.common import utils
25from deckhand import errors 25from deckhand import errors
26 26
@@ -38,7 +38,7 @@ class SecretsManager(object):
38 38
39 @staticmethod 39 @staticmethod
40 def requires_encryption(document): 40 def requires_encryption(document):
41 clazz = document_wrapper.DocumentDict 41 clazz = dd
42 if not isinstance(document, clazz): 42 if not isinstance(document, clazz):
43 document = clazz(document) 43 document = clazz(document)
44 return document.is_encrypted 44 return document.is_encrypted
@@ -64,8 +64,8 @@ class SecretsManager(object):
64 # TODO(fmontei): Look into POSTing Deckhand metadata into Barbican's 64 # TODO(fmontei): Look into POSTing Deckhand metadata into Barbican's
65 # Secrets Metadata API to make it easier to track stale secrets from 65 # Secrets Metadata API to make it easier to track stale secrets from
66 # prior revisions that need to be deleted. 66 # prior revisions that need to be deleted.
67 if not isinstance(secret_doc, document_wrapper.DocumentDict): 67 if not isinstance(secret_doc, dd):
68 secret_doc = document_wrapper.DocumentDict(secret_doc) 68 secret_doc = dd(secret_doc)
69 69
70 if secret_doc.is_encrypted: 70 if secret_doc.is_encrypted:
71 payload = cls.barbican_driver.create_secret(secret_doc) 71 payload = cls.barbican_driver.create_secret(secret_doc)
@@ -100,8 +100,8 @@ class SecretsManager(object):
100 :returns: None 100 :returns: None
101 101
102 """ 102 """
103 if not isinstance(document, document_wrapper.DocumentDict): 103 if not isinstance(document, dd):
104 document = document_wrapper.DocumentDict(document) 104 document = dd(document)
105 105
106 secret_ref = document.data 106 secret_ref = document.data
107 if document.is_encrypted and document.has_barbican_ref: 107 if document.is_encrypted and document.has_barbican_ref:
@@ -116,7 +116,7 @@ class SecretsSubstitution(object):
116 """Class for document substitution logic for YAML files.""" 116 """Class for document substitution logic for YAML files."""
117 117
118 __slots__ = ('_fail_on_missing_sub_src', '_substitution_sources', 118 __slots__ = ('_fail_on_missing_sub_src', '_substitution_sources',
119 '_encryption_sources') 119 '_encryption_sources', '_cleartext_secrets')
120 120
121 _insecure_reg_exps = ( 121 _insecure_reg_exps = (
122 re.compile(r'^.* is not of type .+$'), 122 re.compile(r'^.* is not of type .+$'),
@@ -169,7 +169,9 @@ class SecretsSubstitution(object):
169 return self._encryption_sources[secret_ref] 169 return self._encryption_sources[secret_ref]
170 170
171 def __init__(self, substitution_sources=None, 171 def __init__(self, substitution_sources=None,
172 fail_on_missing_sub_src=True, encryption_sources=None): 172 fail_on_missing_sub_src=True,
173 encryption_sources=None,
174 cleartext_secrets=False):
173 """SecretSubstitution constructor. 175 """SecretSubstitution constructor.
174 176
175 This class will automatically detect documents that require 177 This class will automatically detect documents that require
@@ -187,6 +189,9 @@ class SecretsSubstitution(object):
187 actual unecrypted data. If encrypting data with Barbican, the 189 actual unecrypted data. If encrypting data with Barbican, the
188 reference will be a Barbican secret reference. 190 reference will be a Barbican secret reference.
189 :type encryption_sources: dict 191 :type encryption_sources: dict
192 :param cleartext_secrets: Whether to show unencrypted data as
193 cleartext.
194 :type cleartext_secrets: bool
190 """ 195 """
191 196
192 # This maps a 2-tuple of (schema, name) to a document from which the 197 # This maps a 2-tuple of (schema, name) to a document from which the
@@ -196,14 +201,15 @@ class SecretsSubstitution(object):
196 self._substitution_sources = {} 201 self._substitution_sources = {}
197 self._encryption_sources = encryption_sources or {} 202 self._encryption_sources = encryption_sources or {}
198 self._fail_on_missing_sub_src = fail_on_missing_sub_src 203 self._fail_on_missing_sub_src = fail_on_missing_sub_src
204 self._cleartext_secrets = cleartext_secrets
199 205
200 if isinstance(substitution_sources, dict): 206 if isinstance(substitution_sources, dict):
201 self._substitution_sources = substitution_sources 207 self._substitution_sources = substitution_sources
202 else: 208 else:
203 self._substitution_sources = dict() 209 self._substitution_sources = dict()
204 for document in substitution_sources: 210 for document in substitution_sources:
205 if not isinstance(document, document_wrapper.DocumentDict): 211 if not isinstance(document, dd):
206 document = document_wrapper.DocumentDict(document) 212 document = dd(document)
207 if document.schema and document.name: 213 if document.schema and document.name:
208 self._substitution_sources.setdefault( 214 self._substitution_sources.setdefault(
209 (document.schema, document.name), document) 215 (document.schema, document.name), document)
@@ -235,6 +241,43 @@ class SecretsSubstitution(object):
235 src_doc.name, dest_doc.schema, dest_doc.layer, 241 src_doc.name, dest_doc.schema, dest_doc.layer,
236 dest_doc.name) 242 dest_doc.name)
237 243
244 def _substitute_one(self, document, src_doc, src_secret, dest_path,
245 dest_pattern, dest_recurse=None):
246 dest_recurse = dest_recurse or {}
247 exc_message = ''
248 try:
249 substituted_data = utils.jsonpath_replace(
250 document['data'], src_secret, dest_path,
251 pattern=dest_pattern, recurse=dest_recurse)
252 if (isinstance(document['data'], dict) and
253 isinstance(substituted_data, dict)):
254 document['data'].update(substituted_data)
255 elif substituted_data:
256 document['data'] = substituted_data
257 else:
258 exc_message = (
259 'Failed to create JSON path "%s" in the '
260 'destination document [%s, %s] %s. '
261 'No data was substituted.' % (
262 dest_path, document.schema,
263 document.layer, document.name))
264 except Exception as e:
265 LOG.error('Unexpected exception occurred '
266 'while attempting '
267 'substitution using '
268 'source document [%s, %s] %s '
269 'referenced in [%s, %s] %s. Details: %s',
270 src_doc.schema, src_doc.name, src_doc.layer,
271 document.schema, document.layer,
272 document.name,
273 six.text_type(e))
274 exc_message = six.text_type(e)
275 finally:
276 if exc_message:
277 self._handle_unknown_substitution_exc(
278 exc_message, src_doc, document)
279 return document
280
238 def substitute_all(self, documents): 281 def substitute_all(self, documents):
239 """Substitute all documents that have a `metadata.substitutions` field. 282 """Substitute all documents that have a `metadata.substitutions` field.
240 283
@@ -259,8 +302,8 @@ class SecretsSubstitution(object):
259 documents = [documents] 302 documents = [documents]
260 303
261 for document in documents: 304 for document in documents:
262 if not isinstance(document, document_wrapper.DocumentDict): 305 if not isinstance(document, dd):
263 document = document_wrapper.DocumentDict(document) 306 document = dd(document)
264 # If the document has substitutions include it. 307 # If the document has substitutions include it.
265 if document.substitutions: 308 if document.substitutions:
266 documents_to_substitute.append(document) 309 documents_to_substitute.append(document)
@@ -322,43 +365,26 @@ class SecretsSubstitution(object):
322 dest_pattern = each_dest_path.get('pattern', None) 365 dest_pattern = each_dest_path.get('pattern', None)
323 dest_recurse = each_dest_path.get('recurse', {}) 366 dest_recurse = each_dest_path.get('recurse', {})
324 367
368 # If the source document is encrypted and cleartext_secrets
369 # is False, then redact the substitution metadata in the
370 # destination document to prevent reverse-engineering of
371 # where the sensitive data came from.
372 if src_doc.is_encrypted and not self._cleartext_secrets:
373 sub['src']['path'] = dd.redact(src_path)
374 sub['dest']['path'] = dd.redact(dest_path)
375
325 LOG.debug('Substituting from schema=%s layer=%s name=%s ' 376 LOG.debug('Substituting from schema=%s layer=%s name=%s '
326 'src_path=%s into dest_path=%s, dest_pattern=%s', 377 'src_path=%s into dest_path=%s, dest_pattern=%s',
327 src_schema, src_doc.layer, src_name, src_path, 378 src_schema, src_doc.layer, src_name, src_path,
328 dest_path, dest_pattern) 379 dest_path, dest_pattern)
329 380
330 try: 381 document = self._substitute_one(
331 exc_message = '' 382 document,
332 substituted_data = utils.jsonpath_replace( 383 src_doc=src_doc,
333 document['data'], src_secret, dest_path, 384 src_secret=src_secret,
334 pattern=dest_pattern, recurse=dest_recurse) 385 dest_path=dest_path,
335 if (isinstance(document['data'], dict) and 386 dest_pattern=dest_pattern,
336 isinstance(substituted_data, dict)): 387 dest_recurse=dest_recurse)
337 document['data'].update(substituted_data)
338 elif substituted_data:
339 document['data'] = substituted_data
340 else:
341 exc_message = (
342 'Failed to create JSON path "%s" in the '
343 'destination document [%s, %s] %s. '
344 'No data was substituted.' % (
345 dest_path, document.schema,
346 document.layer, document.name))
347 except Exception as e:
348 LOG.error('Unexpected exception occurred '
349 'while attempting '
350 'substitution using '
351 'source document [%s, %s] %s '
352 'referenced in [%s, %s] %s. Details: %s',
353 src_schema, src_name, src_doc.layer,
354 document.schema, document.layer,
355 document.name,
356 six.text_type(e))
357 exc_message = six.text_type(e)
358 finally:
359 if exc_message:
360 self._handle_unknown_substitution_exc(
361 exc_message, src_doc, document)
362 388
363 yield document 389 yield document
364 390
diff --git a/deckhand/tests/unit/control/test_rendered_documents_controller.py b/deckhand/tests/unit/control/test_rendered_documents_controller.py
index b5fdf4c..0ed4c88 100644
--- a/deckhand/tests/unit/control/test_rendered_documents_controller.py
+++ b/deckhand/tests/unit/control/test_rendered_documents_controller.py
@@ -16,6 +16,7 @@ import yaml
16 16
17import mock 17import mock
18 18
19from deckhand.common.document import DocumentDict as dd
19from deckhand.control import revision_documents 20from deckhand.control import revision_documents
20from deckhand.engine import secrets_manager 21from deckhand.engine import secrets_manager
21from deckhand import errors 22from deckhand import errors
@@ -200,6 +201,10 @@ class TestRenderedDocumentsController(test_base.BaseControllerTest):
200class TestRenderedDocumentsControllerRedaction(test_base.BaseControllerTest): 201class TestRenderedDocumentsControllerRedaction(test_base.BaseControllerTest):
201 202
202 def _test_list_rendered_documents(self, cleartext_secrets): 203 def _test_list_rendered_documents(self, cleartext_secrets):
204 """Validates that destination document that substitutes from an
205 encrypted document is appropriately redacted when ``cleartext_secrets``
206 is True.
207 """
203 rules = { 208 rules = {
204 'deckhand:list_cleartext_documents': '@', 209 'deckhand:list_cleartext_documents': '@',
205 'deckhand:list_encrypted_documents': '@', 210 'deckhand:list_encrypted_documents': '@',
@@ -215,6 +220,7 @@ class TestRenderedDocumentsControllerRedaction(test_base.BaseControllerTest):
215 certificate_data = 'sample-certificate' 220 certificate_data = 'sample-certificate'
216 certificate_ref = ('http://127.0.0.1/key-manager/v1/secrets/%s' 221 certificate_ref = ('http://127.0.0.1/key-manager/v1/secrets/%s'
217 % test_utils.rand_uuid_hex()) 222 % test_utils.rand_uuid_hex())
223 redacted_data = dd.redact(certificate_ref)
218 224
219 doc1 = { 225 doc1 = {
220 'data': certificate_data, 226 'data': certificate_data,
@@ -228,6 +234,11 @@ class TestRenderedDocumentsControllerRedaction(test_base.BaseControllerTest):
228 'layer': 'site'}, 'storagePolicy': 'encrypted', 234 'layer': 'site'}, 'storagePolicy': 'encrypted',
229 'replacement': False}} 235 'replacement': False}}
230 236
237 original_substitutions = [
238 {'dest': {'path': '.'},
239 'src': {'schema': 'deckhand/Certificate/v1',
240 'name': 'example-cert', 'path': '.'}}
241 ]
231 doc2 = {'data': {}, 'schema': 'example/Kind/v1', 242 doc2 = {'data': {}, 'schema': 'example/Kind/v1',
232 'name': 'deckhand-global', 'layer': 'global', 243 'name': 'deckhand-global', 'layer': 'global',
233 'metadata': { 244 'metadata': {
@@ -236,10 +247,8 @@ class TestRenderedDocumentsControllerRedaction(test_base.BaseControllerTest):
236 'layeringDefinition': {'abstract': False, 247 'layeringDefinition': {'abstract': False,
237 'layer': 'global'}, 248 'layer': 'global'},
238 'name': 'deckhand-global', 249 'name': 'deckhand-global',
239 'schema': 'metadata/Document/v1', 'substitutions': [ 250 'schema': 'metadata/Document/v1',
240 {'dest': {'path': '.'}, 251 'substitutions': original_substitutions,
241 'src': {'schema': 'deckhand/Certificate/v1',
242 'name': 'example-cert', 'path': '.'}}],
243 'replacement': False}} 252 'replacement': False}}
244 253
245 payload = [layering_policy, doc1, doc2] 254 payload = [layering_policy, doc1, doc2]
@@ -279,10 +288,15 @@ class TestRenderedDocumentsControllerRedaction(test_base.BaseControllerTest):
279 self.assertTrue(all(map(lambda x: x['data'] == certificate_data, 288 self.assertTrue(all(map(lambda x: x['data'] == certificate_data,
280 rendered_documents))) 289 rendered_documents)))
281 else: 290 else:
282 # Expected redacted data for both documents to be returned - 291 # Expect redacted data for both documents to be returned -
283 # because the destination document should receive redacted data. 292 # because the destination document should receive redacted data.
284 self.assertTrue(all(map(lambda x: x['data'] != certificate_data, 293 self.assertTrue(all(map(lambda x: x['data'] == redacted_data,
285 rendered_documents))) 294 rendered_documents)))
295 destination_doc = next(iter(filter(
296 lambda x: x['metadata']['name'] == 'deckhand-global',
297 rendered_documents)))
298 substitutions = destination_doc['metadata']['substitutions']
299 self.assertNotEqual(original_substitutions, substitutions)
286 300
287 def test_list_rendered_documents_cleartext_secrets_true(self): 301 def test_list_rendered_documents_cleartext_secrets_true(self):
288 self._test_list_rendered_documents(cleartext_secrets=True) 302 self._test_list_rendered_documents(cleartext_secrets=True)
diff --git a/deckhand/tests/unit/engine/test_secrets_manager.py b/deckhand/tests/unit/engine/test_secrets_manager.py
index ac733af..7af4060 100644
--- a/deckhand/tests/unit/engine/test_secrets_manager.py
+++ b/deckhand/tests/unit/engine/test_secrets_manager.py
@@ -176,7 +176,8 @@ class TestSecretsSubstitution(test_base.TestDbBase):
176 self.secrets_factory = factories.DocumentSecretFactory() 176 self.secrets_factory = factories.DocumentSecretFactory()
177 177
178 def _test_doc_substitution(self, document_mapping, substitution_sources, 178 def _test_doc_substitution(self, document_mapping, substitution_sources,
179 expected_data, encryption_sources=None): 179 expected_data, encryption_sources=None,
180 cleartext_secrets=True):
180 payload = self.document_factory.gen_test(document_mapping, 181 payload = self.document_factory.gen_test(document_mapping,
181 global_abstract=False) 182 global_abstract=False)
182 bucket_name = test_utils.rand_name('bucket') 183 bucket_name = test_utils.rand_name('bucket')
@@ -188,7 +189,8 @@ class TestSecretsSubstitution(test_base.TestDbBase):
188 189
189 secret_substitution = secrets_manager.SecretsSubstitution( 190 secret_substitution = secrets_manager.SecretsSubstitution(
190 encryption_sources=encryption_sources, 191 encryption_sources=encryption_sources,
191 substitution_sources=substitution_sources) 192 substitution_sources=substitution_sources,
193 cleartext_secrets=cleartext_secrets)
192 substituted_docs = list(secret_substitution.substitute_all(documents)) 194 substituted_docs = list(secret_substitution.substitute_all(documents))
193 self.assertIn(expected_document, substituted_docs) 195 self.assertIn(expected_document, substituted_docs)
194 196