Merge "Redacts Raw Documents"
This commit is contained in:
commit
7d697012fc
@ -15,6 +15,7 @@
|
|||||||
import collections
|
import collections
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import hashlib
|
||||||
from oslo_serialization import jsonutils as json
|
from oslo_serialization import jsonutils as json
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import six
|
import six
|
||||||
@ -171,6 +172,11 @@ class DocumentDict(dict):
|
|||||||
|
|
||||||
return [DocumentDict(d) for d in documents]
|
return [DocumentDict(d) for d in documents]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def redact(cls, input):
|
||||||
|
return hashlib.sha256(json.dumps(input)
|
||||||
|
.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def document_dict_representer(dumper, data):
|
def document_dict_representer(dumper, data):
|
||||||
return dumper.represent_mapping('tag:yaml.org,2002:map', dict(data))
|
return dumper.represent_mapping('tag:yaml.org,2002:map', dict(data))
|
||||||
|
@ -23,6 +23,7 @@ import jsonpath_ng
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from deckhand.common.document import DocumentDict as document_dict
|
||||||
from deckhand.conf import config
|
from deckhand.conf import config
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
|
|
||||||
@ -381,3 +382,27 @@ def deepfilter(dct, **filters):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def redact_document(document):
|
||||||
|
d = _to_document(document)
|
||||||
|
if d.is_encrypted:
|
||||||
|
document['data'] = document_dict.redact(d.data)
|
||||||
|
if d.substitutions:
|
||||||
|
subs = d.substitutions
|
||||||
|
for s in subs:
|
||||||
|
s['src']['path'] = document_dict.redact(s['src']['path'])
|
||||||
|
s['dest']['path'] = document_dict.redact(s['dest']['path'])
|
||||||
|
document['metadata']['substitutions'] = subs
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def redact_documents(documents):
|
||||||
|
return [redact_document(d) for d in documents]
|
||||||
|
|
||||||
|
|
||||||
|
def _to_document(document):
|
||||||
|
clazz = document_dict
|
||||||
|
if not isinstance(document, clazz):
|
||||||
|
document = clazz(document)
|
||||||
|
return document
|
||||||
|
@ -39,7 +39,7 @@ class RevisionDocumentsResource(api_base.BaseResource):
|
|||||||
@common.sanitize_params([
|
@common.sanitize_params([
|
||||||
'schema', 'metadata.name', 'metadata.layeringDefinition.abstract',
|
'schema', 'metadata.name', 'metadata.layeringDefinition.abstract',
|
||||||
'metadata.layeringDefinition.layer', 'metadata.label',
|
'metadata.layeringDefinition.layer', 'metadata.label',
|
||||||
'status.bucket', 'order', 'sort', 'limit'])
|
'status.bucket', 'order', 'sort', 'limit', 'cleartext-secrets'])
|
||||||
def on_get(self, req, resp, sanitized_params, revision_id):
|
def on_get(self, req, resp, sanitized_params, revision_id):
|
||||||
"""Returns all documents for a `revision_id`.
|
"""Returns all documents for a `revision_id`.
|
||||||
|
|
||||||
@ -54,6 +54,7 @@ class RevisionDocumentsResource(api_base.BaseResource):
|
|||||||
order_by = sanitized_params.pop('order', None)
|
order_by = sanitized_params.pop('order', None)
|
||||||
sort_by = sanitized_params.pop('sort', None)
|
sort_by = sanitized_params.pop('sort', None)
|
||||||
limit = sanitized_params.pop('limit', None)
|
limit = sanitized_params.pop('limit', None)
|
||||||
|
cleartext_secrets = sanitized_params.pop('cleartext-secrets', None)
|
||||||
|
|
||||||
filters = sanitized_params.copy()
|
filters = sanitized_params.copy()
|
||||||
filters['metadata.storagePolicy'] = ['cleartext']
|
filters['metadata.storagePolicy'] = ['cleartext']
|
||||||
@ -68,6 +69,9 @@ class RevisionDocumentsResource(api_base.BaseResource):
|
|||||||
LOG.exception(six.text_type(e))
|
LOG.exception(six.text_type(e))
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
raise falcon.HTTPNotFound(description=e.format_message())
|
||||||
|
|
||||||
|
if cleartext_secrets not in [True, 'true', 'True']:
|
||||||
|
documents = utils.redact_documents(documents)
|
||||||
|
|
||||||
# Sorts by creation date by default.
|
# Sorts by creation date by default.
|
||||||
documents = utils.multisort(documents, sort_by, order_by)
|
documents = utils.multisort(documents, sort_by, order_by)
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
|
@ -84,7 +84,7 @@ Only enforced after ``list_cleartext_documents`` passes.
|
|||||||
|
|
||||||
Conditionally enforced for the endpoints below if any of the documents in the
|
Conditionally enforced for the endpoints below if any of the documents in the
|
||||||
request body have a ``metadata.storagePolicy`` of "encrypted". If policy
|
request body have a ``metadata.storagePolicy`` of "encrypted". If policy
|
||||||
enforcement fails, encrypted documents are exluded from the response.""",
|
enforcement fails, encrypted documents are excluded from the response.""",
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'method': 'GET',
|
'method': 'GET',
|
||||||
|
@ -75,6 +75,8 @@ tests:
|
|||||||
content-type: application/x-yaml
|
content-type: application/x-yaml
|
||||||
response_headers:
|
response_headers:
|
||||||
content-type: application/x-yaml
|
content-type: application/x-yaml
|
||||||
|
query_parameters:
|
||||||
|
cleartext-secrets: 'true'
|
||||||
response_multidoc_jsonpaths:
|
response_multidoc_jsonpaths:
|
||||||
$.`len`: 1
|
$.`len`: 1
|
||||||
# NOTE(felipemonteiro): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
# NOTE(felipemonteiro): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
||||||
|
@ -167,6 +167,7 @@ tests:
|
|||||||
content-type: application/x-yaml
|
content-type: application/x-yaml
|
||||||
query_parameters:
|
query_parameters:
|
||||||
metadata.name: armada-doc
|
metadata.name: armada-doc
|
||||||
|
cleartext-secrets: 'true'
|
||||||
response_multidoc_jsonpaths:
|
response_multidoc_jsonpaths:
|
||||||
$.`len`: 1
|
$.`len`: 1
|
||||||
$.[0].data.`split(:, 0, 1)` + "://" + $.[0].data.`split(/, 2, 3)`: $ENVIRON['TEST_BARBICAN_URL']
|
$.[0].data.`split(:, 0, 1)` + "://" + $.[0].data.`split(/, 2, 3)`: $ENVIRON['TEST_BARBICAN_URL']
|
||||||
|
@ -74,6 +74,7 @@ tests:
|
|||||||
status: 200
|
status: 200
|
||||||
query_parameters:
|
query_parameters:
|
||||||
metadata.name: example-armada-cert
|
metadata.name: example-armada-cert
|
||||||
|
cleartext-secrets: 'true'
|
||||||
response_multidoc_jsonpaths:
|
response_multidoc_jsonpaths:
|
||||||
$.`len`: 1
|
$.`len`: 1
|
||||||
# NOTE(felipemonteiro): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
# NOTE(felipemonteiro): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
||||||
|
@ -180,6 +180,7 @@ tests:
|
|||||||
- example-passphrase
|
- example-passphrase
|
||||||
- example-private-key
|
- example-private-key
|
||||||
- example-public-key
|
- example-public-key
|
||||||
|
cleartext-secrets: 'true'
|
||||||
response_multidoc_jsonpaths:
|
response_multidoc_jsonpaths:
|
||||||
$.`len`: 7
|
$.`len`: 7
|
||||||
# NOTE(felipemonteiro): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
# NOTE(felipemonteiro): jsonpath-rw-ext uses a 1 character separator (rather than allowing a string)
|
||||||
|
@ -16,6 +16,7 @@ import yaml
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from deckhand.common.document import DocumentDict as document_dict
|
||||||
from deckhand.engine import secrets_manager
|
from deckhand.engine import secrets_manager
|
||||||
from deckhand import factories
|
from deckhand import factories
|
||||||
from deckhand.tests.unit.control import base as test_base
|
from deckhand.tests.unit.control import base as test_base
|
||||||
@ -102,6 +103,93 @@ data:
|
|||||||
self.assertEqual(2, len(retrieved_documents))
|
self.assertEqual(2, len(retrieved_documents))
|
||||||
self.assertEqual(expected_data_section, retrieved_documents[0]['data'])
|
self.assertEqual(expected_data_section, retrieved_documents[0]['data'])
|
||||||
|
|
||||||
|
def _setup_payload(self):
|
||||||
|
data = '12345'
|
||||||
|
sub_src = '.source1'
|
||||||
|
sub_dest = '.destination2'
|
||||||
|
secrets_factory = factories.DocumentSecretFactory()
|
||||||
|
payload = [secrets_factory.gen_test('Certificate', 'encrypted')]
|
||||||
|
payload[0]['data'] = data
|
||||||
|
sub1 = {'src': {'schema': 'pegleg/SoftwareVersions/v1', 'name': 'sub1',
|
||||||
|
'path': sub_src}, 'dest': {'path': '.destination1'}}
|
||||||
|
sub2 = {'src': {'schema': 'pegleg/SoftwareVersions/v1', 'name': 'sub2',
|
||||||
|
'path': '.source2'}, 'dest': {'path': sub_dest}}
|
||||||
|
payload[0]['metadata']['substitutions'] = [sub1, sub2]
|
||||||
|
return payload, data, sub_src, sub_dest
|
||||||
|
|
||||||
|
def test_list_encrypted_revision_documents_redacted(self):
|
||||||
|
rules = {'deckhand:list_cleartext_documents': '@',
|
||||||
|
'deckhand:list_encrypted_documents': '@',
|
||||||
|
'deckhand:create_cleartext_documents': '@',
|
||||||
|
'deckhand:create_encrypted_documents': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
# Create a document for a bucket.
|
||||||
|
payload, data, sub_src, sub_dest = self._setup_payload()
|
||||||
|
|
||||||
|
with mock.patch.object(secrets_manager, 'SecretsManager',
|
||||||
|
autospec=True) as mock_secrets_mgr:
|
||||||
|
mock_secrets_mgr.create.return_value = payload[0]['data']
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/buckets/mop/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body=yaml.safe_dump_all(payload))
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
revision_id = list(yaml.safe_load_all(resp.text))[0]['status'][
|
||||||
|
'revision']
|
||||||
|
|
||||||
|
# Verify that the created document was redacted.
|
||||||
|
redacted_data = document_dict.redact(data)
|
||||||
|
redacted_sub_src = document_dict.redact(sub_src)
|
||||||
|
redacted_sub_dest = document_dict.redact(sub_dest)
|
||||||
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/revisions/%s/documents' % revision_id,
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
query_string='cleartext-secrets=false')
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
self.assertNotEqual(list(yaml.safe_load_all(resp.text)), [])
|
||||||
|
response_yaml = list(yaml.safe_load_all(resp.text))
|
||||||
|
self.assertEqual(redacted_data, response_yaml[0]['data'])
|
||||||
|
subs = response_yaml[0]['metadata']['substitutions']
|
||||||
|
self.assertEqual(redacted_sub_src, subs[0]['src']['path'])
|
||||||
|
self.assertEqual(redacted_sub_dest, subs[1]['dest']['path'])
|
||||||
|
|
||||||
|
def test_list_encrypted_revision_documents_cleartext_secrets(self):
|
||||||
|
rules = {'deckhand:list_cleartext_documents': '@',
|
||||||
|
'deckhand:list_encrypted_documents': '@',
|
||||||
|
'deckhand:create_cleartext_documents': '@',
|
||||||
|
'deckhand:create_encrypted_documents': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
# Create a document for a bucket.
|
||||||
|
payload, data, sub_src, sub_dest = self._setup_payload()
|
||||||
|
|
||||||
|
with mock.patch.object(secrets_manager, 'SecretsManager',
|
||||||
|
autospec=True) as mock_secrets_mgr:
|
||||||
|
mock_secrets_mgr.create.return_value = payload[0]['data']
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/buckets/mop/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body=yaml.safe_dump_all(payload))
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
revision_id = list(yaml.safe_load_all(resp.text))[0]['status'][
|
||||||
|
'revision']
|
||||||
|
|
||||||
|
# Verify that the created document was not redacted.
|
||||||
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/revisions/%s/documents' % revision_id,
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
query_string='cleartext-secrets=true')
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
self.assertNotEqual(list(yaml.safe_load_all(resp.text)), [])
|
||||||
|
response_yaml = list(yaml.safe_load_all(resp.text))
|
||||||
|
self.assertEqual(data, response_yaml[0]['data'])
|
||||||
|
subs = response_yaml[0]['metadata']['substitutions']
|
||||||
|
self.assertEqual(sub_src, subs[0]['src']['path'])
|
||||||
|
self.assertEqual(sub_dest, subs[1]['dest']['path'])
|
||||||
|
|
||||||
|
|
||||||
class TestRevisionDocumentsControllerNegativeRBAC(
|
class TestRevisionDocumentsControllerNegativeRBAC(
|
||||||
test_base.BaseControllerTest):
|
test_base.BaseControllerTest):
|
||||||
|
@ -156,7 +156,7 @@ class RealPolicyFixture(fixtures.Fixture):
|
|||||||
def enforce_policy_and_remember_actual_rules(
|
def enforce_policy_and_remember_actual_rules(
|
||||||
action, *a, **k):
|
action, *a, **k):
|
||||||
self.actual_policy_actions.append(action)
|
self.actual_policy_actions.append(action)
|
||||||
_do_enforce_rbac(action, *a, **k)
|
return _do_enforce_rbac(action, *a, **k)
|
||||||
|
|
||||||
mock_do_enforce_rbac = mock.patch.object(
|
mock_do_enforce_rbac = mock.patch.object(
|
||||||
deckhand.policy, '_do_enforce_rbac', autospec=True).start()
|
deckhand.policy, '_do_enforce_rbac', autospec=True).start()
|
||||||
|
@ -88,6 +88,9 @@ Supported query string parameters:
|
|||||||
descending order.
|
descending order.
|
||||||
* ``limit`` - int, optional - Controls number of documents returned by this
|
* ``limit`` - int, optional - Controls number of documents returned by this
|
||||||
endpoint.
|
endpoint.
|
||||||
|
* ``cleartext-secrets`` - boolean, optional - Determines if data and substitutions
|
||||||
|
paths should be redacted (sha256) if a user has access to encrypted files.
|
||||||
|
Default is to redact the values.
|
||||||
|
|
||||||
GET ``/revisions/{revision_id}/rendered-documents``
|
GET ``/revisions/{revision_id}/rendered-documents``
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
# documents in the
|
# documents in the
|
||||||
# request body have a ``metadata.storagePolicy`` of "encrypted". If
|
# request body have a ``metadata.storagePolicy`` of "encrypted". If
|
||||||
# policy
|
# policy
|
||||||
# enforcement fails, encrypted documents are exluded from the
|
# enforcement fails, encrypted documents are excluded from the
|
||||||
# response.
|
# response.
|
||||||
# GET api/v1.0/revisions/{revision_id}/documents
|
# GET api/v1.0/revisions/{revision_id}/documents
|
||||||
# GET api/v1.0/revisions/{revision_id}/rendered-documents
|
# GET api/v1.0/revisions/{revision_id}/rendered-documents
|
||||||
|
Loading…
Reference in New Issue
Block a user