def test_list_revision_documents_sorting_by_schema_then_limit(self):
        rules = {'deckhand:list_cleartext_documents': '@',
                 'deckhand:list_encrypted_documents': '@',
                 'deckhand:create_cleartext_documents': '@'}
        self.policy.set_rules(rules)

        documents_factory = factories.DocumentFactory(2, [1, 1])
        documents = documents_factory.gen_test({
            '_SITE_ACTIONS_1_': {
                'actions': [{'method': 'merge', 'path': '.'}]
            }
        })
        schemas = ['deckhand/Certificate/v1',
                   'deckhand/CertificateKey/v1',
                   'deckhand/LayeringPolicy/v1']
        for idx in range(len(documents)):
            documents[idx]['schema'] = schemas[idx]

        for limit in (0, 1, 2, 3):
            expected_schemas = schemas[:limit]

            resp = self.app.simulate_put(
                '/api/v1.0/buckets/mop/documents',
                headers={'Content-Type': 'application/x-yaml'},
                body=yaml.safe_dump_all(documents))
            self.assertEqual(200, resp.status_code)
            revision_id = list(yaml.safe_load_all(resp.text))[0]['status'][
                'revision']

            resp = self.app.simulate_get(
                '/api/v1.0/revisions/%s/documents' % revision_id,
                params={'sort': 'schema', 'limit': limit}, params_csv=False,
                headers={'Content-Type': 'application/x-yaml'})
            self.assertEqual(200, resp.status_code)
            retrieved_documents = list(yaml.safe_load_all(resp.text))

            self.assertEqual(limit, len(retrieved_documents))
            self.assertEqual(expected_schemas,
                             [d['schema'] for d in retrieved_documents])
示例#2
0
    def test_layering_documents_with_different_schemas(self):
        """Validate that attempting to layer documents with different schemas
        results in errors.
        """
        doc_factory = factories.DocumentFactory(3, [1, 1, 1])
        documents = doc_factory.gen_test({})

        # Region and site documents should result in no parent being found
        # since their schemas will not match that of their parent's.
        for idx in range(2, 4):  # Only region/site have parent.
            prev_schema = documents[idx]['schema']
            documents[idx]['schema'] = test_utils.rand_name('schema')

            # Escape '[' and ']' for regex to work.
            expected_err = ("Missing parent document for document %s." %
                            documents[idx]).replace('[',
                                                    '\[').replace(']', '\]')
            self.assertRaisesRegex(errors.MissingDocumentParent, expected_err,
                                   layering.DocumentLayering, documents)

            # Restore schema for next test run.
            documents[idx]['schema'] = prev_schema
示例#3
0
    def test_lookup_by_revision_id_cache(self):
        """Validate ``lookup_by_revision_id`` caching works.

        Passing in None in lieu of the actual documents proves that:

        * if the payload is in the cache, then no error is thrown since the
          cache is hit so no further processing is performed, where otherwise a
          method would be called on `None`
        * if the payload is not in the cache, then following logic above,
          method is called on `None`, raising AttributeError
        """

        document_factory = factories.DocumentFactory(1, [1])
        documents = document_factory.gen_test({})

        # Validate that caching the ref returns expected payload.
        rendered_documents, cache_hit = cache.lookup_by_revision_id(
            1, documents)
        self.assertIsInstance(rendered_documents, list)
        self.assertFalse(cache_hit)

        # Validate that the cache actually works.
        next_rendered_documents, cache_hit = cache.lookup_by_revision_id(
            1, None)
        self.assertEqual(rendered_documents, next_rendered_documents)
        self.assertTrue(cache_hit)

        # No documents passed in and revision ID 2 isn't cached - so expect
        # this to blow up.
        with testtools.ExpectedException(AttributeError):
            cache.lookup_by_revision_id(2, None)

        # Invalidate the cache and ensure the original data isn't there.
        cache.invalidate()

        # The cache won't be hit this time - expect AttributeError.
        with testtools.ExpectedException(AttributeError):
            cache.lookup_by_revision_id(1, None)
示例#4
0
    def test_list_cleartext_rendered_documents_insufficient_permissions(self):
        rules = {
            'deckhand:list_cleartext_documents': 'rule:admin_api',
            'deckhand:create_cleartext_documents': '@'
        }
        self.policy.set_rules(rules)

        # Create a document for a bucket.
        documents_factory = factories.DocumentFactory(1, [1])
        payload = [documents_factory.gen_test({})[0]]
        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 returned.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/rendered-documents' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(403, resp.status_code)
示例#5
0
    def test_layering_invalid_substitution_format_raises_exc(self):
        doc_factory = factories.DocumentFactory(1, [1])
        layering_policy, document_template = doc_factory.gen_test(
            {
                "_GLOBAL_SUBSTITUTIONS_1_": [{
                    "dest": {
                        "path": ".c"
                    },
                    "src": {
                        "schema": "deckhand/Certificate/v1",
                        "name": "global-cert",
                        "path": "."
                    }
                }],
            },
            global_abstract=False)

        for key in ('src', 'dest'):
            document = copy.deepcopy(document_template)
            del document['metadata']['substitutions'][0][key]
            self.assertRaises(errors.InvalidDocumentFormat,
                              self._test_layering, [layering_policy, document],
                              validate=True)

        for key in ('schema', 'name', 'path'):
            document = copy.deepcopy(document_template)
            del document['metadata']['substitutions'][0]['src'][key]
            self.assertRaises(errors.InvalidDocumentFormat,
                              self._test_layering, [layering_policy, document],
                              validate=True)

        for key in ('path', ):
            document = copy.deepcopy(document_template)
            del document['metadata']['substitutions'][0]['dest'][key]
            self.assertRaises(errors.InvalidDocumentFormat,
                              self._test_layering, [layering_policy, document],
                              validate=True)
示例#6
0
    def test_layering_two_concrete_regions_one_child_each(self):
        """Scenario:

        Initially: r1: {"c": 3, "d": 4}, r2: {"e": 5, "f": 6}
        Merge "." (g -> r1): {"a": 1, "b": 2, "c": 3, "d": 4}
        Merge "." (r1 -> s1): {"a": 1, "b": 2, "c": 3, "d": 4, "g": 7, "h": 8}
        Merge "." (g -> r2): {"a": 1, "b": 2, "e": 5, "f": 6}
        Merge "." (r2 -> s2): {"a": 1, "b": 2, "e": 5, "f": 6, "i": 9, "j": 10}
        """
        mapping = {
            "_GLOBAL_DATA_1_": {"data": {"a": 1, "b": 2}},
            "_REGION_DATA_1_": {"data": {"c": 3, "d": 4}},
            "_REGION_ACTIONS_1_": {
                "actions": [{"method": "merge", "path": "."}]},
            "_REGION_DATA_2_": {"data": {"e": 5, "f": 6}},
            "_REGION_ACTIONS_2_": {
                "actions": [{"method": "merge", "path": "."}]},
            "_SITE_DATA_1_": {"data": {"g": 7, "h": 8}},
            "_SITE_ACTIONS_1_": {
                "actions": [{"method": "merge", "path": "."}]},
            "_SITE_DATA_2_": {"data": {"i": 9, "j": 10}},
            "_SITE_ACTIONS_2_": {
                "actions": [{"method": "merge", "path": "."}]}
        }
        doc_factory = factories.DocumentFactory(3, [1, 2, 2])
        documents = doc_factory.gen_test(
            mapping, region_abstract=False, site_abstract=False,
            site_parent_selectors=[
                {'region': 'region1'}, {'region': 'region2'}])

        site_expected = [{"a": 1, "b": 2, "c": 3, "d": 4, "g": 7, "h": 8},
                         {"a": 1, "b": 2, "e": 5, "f": 6, "i": 9, "j": 10}]
        region_expected = [{"a": 1, "b": 2, "c": 3, "d": 4},
                           {"a": 1, "b": 2, "e": 5, "f": 6}]
        global_expected = {"a": 1, "b": 2}
        self._test_layering(documents, site_expected, region_expected,
                            global_expected)
示例#7
0
    def test_layering_site_and_global_abstract(self):
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    },
                    "c": 9
                }
            },
            "_SITE_DATA_1_": {
                "data": {
                    "a": {
                        "x": 7,
                        "z": 3
                    },
                    "b": 4
                }
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "delete",
                    "path": '.a'
                }]
            }
        }
        doc_factory = factories.DocumentFactory(2, [1, 1])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=True,
                                         global_abstract=True)

        site_expected = {"a": {"x": 7, "z": 3}, "b": 4}
        global_expected = {'a': {'x': 1, 'y': 2}, 'c': 9}
        self._test_layering(documents,
                            site_expected,
                            global_expected=global_expected)
示例#8
0
    def test_layering_site_region_and_global_concrete(self):
        # Both the site and region data should be updated as they're both
        # concrete docs.
        mapping = {
            "_GLOBAL_DATA_1_": {"data": {"a": {"x": 1, "y": 2}}},
            "_REGION_DATA_1_": {"data": {"a": {"z": 3}, "b": 5}},
            "_SITE_DATA_1_": {"data": {"b": 4}},
            "_REGION_ACTIONS_1_": {
                "actions": [{"method": "replace", "path": ".a"}]},
            "_SITE_ACTIONS_1_": {
                "actions": [{"method": "merge", "path": "."}]}
        }
        doc_factory = factories.DocumentFactory(3, [1, 1, 1])
        documents = doc_factory.gen_test(
            mapping, site_abstract=False, region_abstract=False,
            global_abstract=False)

        site_expected = {'a': {'z': 3}, 'b': 4}
        region_expected = {'a': {'z': 3}}
        # Global data remains unchanged as there's no layer higher than it in
        # this example.
        global_expected = {'a': {'x': 1, 'y': 2}}
        self._test_layering(documents, site_expected, region_expected,
                            global_expected)
示例#9
0
    def test_layering_multiple_delete(self):
        """Scenario:

        Initially: {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}}
        Delete ".": {}
        Delete ".": {}
        Merge ".": {'b': 4}
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {'a': {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}}},
            "_REGION_DATA_1_": {"data": {"a": {"z": 3}}},
            "_SITE_DATA_1_": {"data": {"b": 4}},
            "_REGION_ACTIONS_1_": {
                "actions": [{'path': '.', 'method': 'delete'},
                            {'path': '.', 'method': 'delete'}]},
            "_SITE_ACTIONS_1_": {
                "actions": [{"method": "merge", "path": "."}]}
        }
        doc_factory = factories.DocumentFactory(3, [1, 1, 1])
        documents = doc_factory.gen_test(mapping, site_abstract=False)

        site_expected = {'b': 4}
        self._test_layering(documents, site_expected)
示例#10
0
    def test_list_encrypted_rendered_documents_insufficient_permissions(self):
        rules = {
            'deckhand:list_cleartext_documents': '@',
            'deckhand:list_encrypted_documents': 'rule:admin_api',
            'deckhand:create_cleartext_documents': '@',
            'deckhand:create_encrypted_documents': '@'
        }
        self.policy.set_rules(rules)

        # Create a document for a bucket.
        documents_factory = factories.DocumentFactory(1, [1])
        layering_policy = documents_factory.gen_test({})[0]
        secrets_factory = factories.DocumentSecretFactory()
        encrypted_document = secrets_factory.gen_test('Certificate',
                                                      'encrypted')
        payload = [layering_policy, encrypted_document]

        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 returned.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/rendered-documents' % revision_id,
            headers={'Content-Type': 'application/x-yaml'},
            params={'schema': encrypted_document['schema']})
        self.assertEqual(200, resp.status_code)
        self.assertEmpty(list(yaml.safe_load_all(resp.text)))
示例#11
0
    def test_layering_multiple_replace_2(self):
        """Scenario:

        Initially: {'a': {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}}
        Replace ".a": {'a': {'z': 5}, 'b': {'v': 3, 'w': 4}}
        Replace ".b": {'a': {'z': 5}, 'b': [109]}
        Merge ".": {'a': {'z': 5}, 'b': [32]}
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {'a': {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}}},
            "_REGION_DATA_1_": {"data": {'a': {'z': 5}, 'b': [109]}},
            "_SITE_DATA_1_": {"data": {"b": [32]}},
            "_REGION_ACTIONS_1_": {
                "actions": [{'path': '.a', 'method': 'replace'},
                            {'path': '.b', 'method': 'replace'}]},
            "_SITE_ACTIONS_1_": {
                "actions": [{"method": "merge", "path": "."}]}
        }
        doc_factory = factories.DocumentFactory(3, [1, 1, 1])
        documents = doc_factory.gen_test(mapping, site_abstract=False)

        site_expected = {'a': {'z': 5}, 'b': [32]}
        self._test_layering(documents, site_expected)
    def test_layering_with_substitution_cycle_fails(self):
        """Validate that a substitution dependency cycle raises a critical
        failure.

        In the case below, the cycle exists between
        site-1 -> site-2 -> site-3 -> site-1
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_SITE_NAME_1_":
            "site-1",
            "_SITE_DATA_1_": {
                "data": {
                    "c": "placeholder"
                }
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".c"
                },
                "src": {
                    "schema": "example/Kind/v1",
                    "name": "site-3",
                    "path": "."
                }
            }],
            "_SITE_NAME_2_":
            "site-2",
            "_SITE_DATA_2_": {
                "data": {
                    "d": "placeholder"
                }
            },
            "_SITE_ACTIONS_2_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_2_": [{
                "dest": {
                    "path": ".d"
                },
                "src": {
                    "schema": "example/Kind/v1",
                    "name": "site-1",
                    "path": ".c"
                }
            }],
            "_SITE_NAME_3_":
            "site-3",
            "_SITE_DATA_3_": {
                "data": {
                    "e": "placeholder"
                }
            },
            "_SITE_ACTIONS_3_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_3_": [{
                "dest": {
                    "path": ".e"
                },
                "src": {
                    "schema": "example/Kind/v1",
                    "name": "site-2",
                    "path": ".d"
                }
            }]
        }
        doc_factory = factories.DocumentFactory(2, [1, 3])
        documents = doc_factory.gen_test(mapping, site_abstract=False)

        # Pass in the documents in reverse order to ensure that the dependency
        # chain by default is not linear and thus requires sorting.
        self.assertRaises(errors.SubstitutionDependencyCycle,
                          layering.DocumentLayering,
                          documents,
                          substitution_sources=documents)
    def test_layering_with_substitution_dependency_chain(self):
        """Validate that parent with multiple children that substitute from
        each other works no matter the order of the documents.
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_GLOBAL_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".b"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "global-cert",
                    "path": "."
                }
            }],
            "_SITE_NAME_1_":
            "site-1",
            "_SITE_DATA_1_": {
                "data": {
                    "c": "placeholder"
                }
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".c"
                },
                "src": {
                    "schema": "deckhand/CertificateKey/v1",
                    "name": "site-cert",
                    "path": "."
                }
            }],
            "_SITE_NAME_2_":
            "site-2",
            "_SITE_DATA_2_": {
                "data": {
                    "d": "placeholder"
                }
            },
            "_SITE_ACTIONS_2_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_2_": [{
                "dest": {
                    "path": ".d"
                },
                "src": {
                    "schema": "example/Kind/v1",
                    "name": "site-1",
                    "path": ".c"
                }
            }],
            "_SITE_NAME_3_":
            "site-3",
            "_SITE_DATA_3_": {
                "data": {
                    "e": "placeholder"
                }
            },
            "_SITE_ACTIONS_3_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_3_": [{
                "dest": {
                    "path": ".e"
                },
                "src": {
                    "schema": "example/Kind/v1",
                    "name": "site-2",
                    "path": ".d"
                }
            }]
        }
        doc_factory = factories.DocumentFactory(2, [1, 3])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         global_abstract=False)
        secrets_factory = factories.DocumentSecretFactory()

        global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'}
        site_expected = [{
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 'global-secret',
            'c': 'site-secret'
        }, {
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 'global-secret',
            'd': 'site-secret'
        }, {
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 'global-secret',
            'e': 'site-secret'
        }]

        certificate = secrets_factory.gen_test('Certificate',
                                               'cleartext',
                                               data='global-secret',
                                               name='global-cert')
        certificate_key = secrets_factory.gen_test('CertificateKey',
                                                   'cleartext',
                                                   data='site-secret',
                                                   name='site-cert')
        documents.extend([certificate] + [certificate_key])

        # Pass in the documents in reverse order to ensure that the dependency
        # chain by default is not linear and thus requires sorting.
        self._test_layering(documents,
                            site_expected=site_expected,
                            global_expected=global_expected,
                            strict=False)

        # Try different permutations of document orders for good measure.
        for documents in list(itertools.permutations(documents))[:10]:
            self._test_layering(documents,
                                site_expected=site_expected,
                                global_expected=global_expected,
                                strict=False)
    def test_parent_and_child_undergo_layering_and_substitution_empty_layers(
            self, mock_log):
        """Validate that parent and child documents both undergo substitution
        and layering.

        empty layer -> discard
          |
          v
        empty layer -> discard
          |
          v
        global -> requires substitution
          |
          v
        empty layer -> discard
          |
          V
        site -> requires substitution (layered with global)

        Where the site's parent is actually the global document.
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_GLOBAL_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".b"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "global-cert",
                    "path": "."
                }
            }],
            "_SITE_DATA_1_": {
                "data": {
                    "c": "need-site-secret"
                }
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".c"
                },
                "src": {
                    "schema": "deckhand/CertificateKey/v1",
                    "name": "site-cert",
                    "path": "."
                }
            }],
        }
        doc_factory = factories.DocumentFactory(2, [1, 1])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         global_abstract=False)
        documents[0]['data']['layerOrder'] = [
            'empty_1', 'empty_2', 'global', 'empty_3', 'site'
        ]
        secrets_factory = factories.DocumentSecretFactory()

        global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'}
        site_expected = {
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 'global-secret',
            'c': 'site-secret'
        }

        certificate = secrets_factory.gen_test('Certificate',
                                               'cleartext',
                                               data='global-secret',
                                               name='global-cert')
        certificate_key = secrets_factory.gen_test('CertificateKey',
                                                   'cleartext',
                                                   data='site-secret',
                                                   name='site-cert')
        documents.extend([certificate] + [certificate_key])

        self._test_layering(documents,
                            site_expected=site_expected,
                            global_expected=global_expected,
                            strict=False)

        expected_message = (
            '%s is an empty layer with no documents. It will be discarded '
            'from the layerOrder during the layering process.')
        expected_log_calls = [
            mock.call(expected_message, layer)
            for layer in ('empty_1', 'empty_2', 'empty_3')
        ]
        mock_log.info.assert_has_calls(expected_log_calls)
    def test_parent_with_multi_child_layering_and_multi_substitutions(self):
        """Validate that parent and children documents both undergo layering
        and multiple substitutions.

        global -> requires substitution
          |
          v
        site1 -> requires multiple substitutions
        site2 -> requires multiple substitutions
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_GLOBAL_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".b"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "global-cert",
                    "path": "."
                }
            }],
            "_SITE_DATA_1_": {
                "data": {}
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".c"
                },
                "src": {
                    "schema": "deckhand/CertificateKey/v1",
                    "name": "site-1-cert-key",
                    "path": "."
                },
            }, {
                "dest": {
                    "path": ".d"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "site-1-cert",
                    "path": "."
                }
            }],
            "_SITE_DATA_2_": {
                "data": {}
            },
            "_SITE_ACTIONS_2_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_2_": [{
                "dest": {
                    "path": ".e"
                },
                "src": {
                    "schema": "deckhand/CertificateKey/v1",
                    "name": "site-2-cert-key",
                    "path": "."
                },
            }, {
                "dest": {
                    "path": ".f"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "site-2-cert",
                    "path": "."
                }
            }]
        }
        doc_factory = factories.DocumentFactory(2, [1, 2])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         global_abstract=False)
        secrets_factory = factories.DocumentSecretFactory()

        global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'}
        site_expected = [{
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 'global-secret',
            'c': 'site-1-sec-key',
            'd': 'site-1-sec'
        }, {
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 'global-secret',
            'e': 'site-2-sec-key',
            'f': 'site-2-sec'
        }]

        certificate = secrets_factory.gen_test('Certificate',
                                               'cleartext',
                                               data='global-secret',
                                               name='global-cert')
        certificate_keys = [
            secrets_factory.gen_test('CertificateKey',
                                     'cleartext',
                                     data='site-%d-sec-key' % idx,
                                     name='site-%d-cert-key' % idx)
            for idx in range(1, 3)
        ]
        certificates = [
            secrets_factory.gen_test('Certificate',
                                     'cleartext',
                                     data='site-%d-sec' % idx,
                                     name='site-%d-cert' % idx)
            for idx in range(1, 3)
        ]
        documents.extend([certificate] + certificate_keys + certificates)

        self._test_layering(documents,
                            site_expected=site_expected,
                            global_expected=global_expected,
                            strict=False)
    def test_parent_and_child_layering_and_substitution_same_paths(self):
        """Validate that parent and child documents both undergo layering and
        substitution where the substitution occurs at the same path.

        global -> requires substitution
          |
          v
        site -> requires substitution
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_GLOBAL_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".b"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "global-cert",
                    "path": "."
                }
            }],
            "_SITE_DATA_1_": {
                "data": {}
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".b"
                },
                "src": {
                    "schema": "deckhand/CertificateKey/v1",
                    "name": "site-cert",
                    "path": "."
                }
            }],
        }
        doc_factory = factories.DocumentFactory(2, [1, 1])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         global_abstract=False)
        secrets_factory = factories.DocumentSecretFactory()

        global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'}
        site_expected = {'a': {'x': 1, 'y': 2}, 'b': 'site-secret'}

        certificate = secrets_factory.gen_test('Certificate',
                                               'cleartext',
                                               data='global-secret',
                                               name='global-cert')
        certificate_key = secrets_factory.gen_test('CertificateKey',
                                                   'cleartext',
                                                   data='site-secret',
                                                   name='site-cert')
        documents.extend([certificate, certificate_key])

        self._test_layering(documents,
                            site_expected=site_expected,
                            global_expected=global_expected,
                            strict=False)
示例#17
0
    def test_validation_with_registered_data_schema_expect_multi_failure(self):
        rules = {
            'deckhand:create_cleartext_documents': '@',
            'deckhand:list_validations': '@',
            'deckhand:show_validation': '@'
        }
        self.policy.set_rules(rules)

        # Create a `DataSchema` against which the test document will be
        # validated.
        data_schema_factory = factories.DataSchemaFactory()
        metadata_name = 'example/foo/v1'
        schema_to_use = {
            '$schema': 'http://json-schema.org/schema#',
            'type': 'object',
            'properties': {
                'a': {
                    'type': 'integer'  # Test doc will fail b/c of wrong type.
                }
            },
            'required': ['a']
        }
        data_schema = data_schema_factory.gen_test(metadata_name,
                                                   data=schema_to_use)

        # Failure #1.
        # Create the test document that fails the validation due to the
        # schema defined by the `DataSchema` document.
        doc_factory = factories.DocumentFactory(1, [1])
        doc_to_test = doc_factory.gen_test(
            {'_GLOBAL_DATA_1_': {
                'data': {
                    'a': 'fail'
                }
            }},
            global_abstract=False)[-1]
        doc_to_test['schema'] = 'example/foo/v1'
        doc_to_test['metadata']['name'] = 'test_doc'
        # Failure #2.
        # Remove required metadata property, causing error to be generated.
        del doc_to_test['metadata']['layeringDefinition']

        revision_id = self._create_revision(payload=[doc_to_test, data_schema])

        # Validate that the validation was created and reports failure.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            1,
            'results': [{
                'name': types.DECKHAND_SCHEMA_VALIDATION,
                'status': 'failure'
            }]
        }
        self.assertEqual(expected_body, body)

        # Validate that both expected errors are present for validation.
        expected_errors = [{
            'error_section': {
                'data': {
                    'a': 'fail'
                },
                'metadata': {
                    'labels': {
                        'global': 'global1'
                    },
                    'name': 'test_doc',
                    'schema': 'metadata/Document/v1.0'
                },
                'schema': 'example/foo/v1'
            },
            'name': 'test_doc',
            'path': '.metadata',
            'schema': 'example/foo/v1',
            'message': "'layeringDefinition' is a required property",
            'validation_schema': document_schema.schema,
            'schema_path': '.properties.metadata.required'
        }, {
            'error_section': {
                'a': 'fail'
            },
            'name': 'test_doc',
            'path': '.data.a',
            'schema': 'example/foo/v1',
            'message': "'fail' is not of type 'integer'",
            'validation_schema': schema_to_use,
            'schema_path': '.properties.a.type'
        }]
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations/%s/entries/0' %
            (revision_id, types.DECKHAND_SCHEMA_VALIDATION),
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)

        self.assertEqual('failure', body['status'])
        self.assertEqual(expected_errors, body['errors'])
示例#18
0
    def test_document_without_data_section_saves_but_fails_validation(self):
        """Validate that a document without the data section is saved to the
        database, but fails validation. This is a valid use case because a
        document in a bucket can be created without a data section, which
        depends on substitution from another document.
        """
        rules = {
            'deckhand:create_cleartext_documents': '@',
            'deckhand:list_validations': '@',
            'deckhand:show_validation': '@'
        }
        self.policy.set_rules(rules)

        documents_factory = factories.DocumentFactory(1, [1])
        document = documents_factory.gen_test({}, global_abstract=False)[-1]
        del document['data']

        data_schema_factory = factories.DataSchemaFactory()
        data_schema = data_schema_factory.gen_test(document['schema'], {})

        revision_id = self._create_revision(payload=[document, data_schema])

        # Validate that the entry is present.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations/%s' %
            (revision_id, types.DECKHAND_SCHEMA_VALIDATION),
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)

        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            2,
            'results': [
                {
                    'id': 0,
                    'status': 'failure'
                },  # Document.
                {
                    'id': 1,
                    'status': 'success'
                }
            ]  # DataSchema.
        }
        self.assertEqual(expected_body, body)

        # Validate that the created document failed validation for the expected
        # reason.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations/%s/entries/0' %
            (revision_id, types.DECKHAND_SCHEMA_VALIDATION),
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_errors = [{
            'error_section': {
                'data': None,
                'metadata': {
                    'labels': {
                        'global': 'global1'
                    },
                    'layeringDefinition': {
                        'abstract': False,
                        'actions': [],
                        'layer': 'global'
                    },
                    'name': document['metadata']['name'],
                    'schema': 'metadata/Document/v1.0'
                },
                'schema': document['schema']
            },
            'name':
            document['metadata']['name'],
            'path':
            '.data',
            'schema':
            document['schema'],
            'message':
            ("None is not of type 'string', 'integer', 'array', 'object'"),
            'validation_schema':
            document_schema.schema,
            'schema_path':
            '.properties.data.type'
        }]
        self.assertIn('errors', body)
        self.assertEqual(expected_errors, body['errors'])
示例#19
0
    def test_layering_two_parents_one_child_each_2(self):
        """Scenario:

        Initially: p1: {"a": {"x": 1, "y": 2}}, p2: {"b": {"f": -9, "g": 71}}
        Where: c1 references p1 and c2 references p2
        Merge "." (p1 -> c1): {"a": {"x": 1, "y": 2, "b": 4}}
        Merge "." (p2 -> c2): {"b": {"f": -9, "g": 71}, "c": 3}
        Delete ".c" (p2 -> c2): {"b": {"f": -9, "g": 71}}
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_GLOBAL_DATA_2_": {
                "data": {
                    "b": {
                        "f": -9,
                        "g": 71
                    }
                }
            },
            "_SITE_DATA_1_": {
                "data": {
                    "b": 4
                }
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_DATA_2_": {
                "data": {
                    "c": 3
                }
            },
            "_SITE_ACTIONS_2_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }, {
                    "method": "delete",
                    "path": ".c"
                }]
            }
        }
        doc_factory = factories.DocumentFactory(2, [2, 2])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         site_parent_selectors=[{
                                             'global':
                                             'global1'
                                         }, {
                                             'global':
                                             'global2'
                                         }])

        site_expected = [{
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 4
        }, {
            "b": {
                "f": -9,
                "g": 71
            }
        }]
        self._test_layering(documents, site_expected)
示例#20
0
    def test_list_rendered_documents_multiple_buckets(self):
        """Validates that only the documents from the most recent revision
        for each bucket in the DB are used for layering.
        """
        rules = {
            'deckhand:list_cleartext_documents': '@',
            'deckhand:list_encrypted_documents': '@',
            'deckhand:create_cleartext_documents': '@'
        }
        self.policy.set_rules(rules)

        bucket_names = ['first', 'first', 'second', 'second']

        # Create 2 documents for each revision. (1 `LayeringPolicy` is created
        # during the very 1st revision). Total = 9.
        for x in range(4):
            bucket_name = bucket_names[x]
            documents_factory = factories.DocumentFactory(2, [1, 1])
            payload = documents_factory.gen_test(
                {
                    '_SITE_ACTIONS_1_': {
                        'actions': [{
                            'method': 'merge',
                            'path': '.'
                        }]
                    }
                },
                global_abstract=False,
                site_abstract=False)
            # Fix up the labels so that each document has a unique parent to
            # avoid layering errors.
            payload[-2]['metadata']['labels'] = {'global': bucket_name}
            payload[-1]['metadata']['layeringDefinition']['parentSelector'] = {
                'global': bucket_name
            }

            if x > 0:
                payload = payload[1:]
            resp = self.app.simulate_put(
                '/api/v1.0/buckets/%s/documents' % bucket_name,
                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']

        # Although 9 documents have been created, 4 of those documents are
        # stale: they were created in older revisions, so expect 5 documents.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/rendered-documents' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)

        documents = list(yaml.safe_load_all(resp.text))
        documents = sorted(documents, key=lambda x: x['status']['bucket'])

        # Validate that the LayeringPolicy was returned, then remove it
        # from documents to validate the rest of them.
        layering_policies = [
            d for d in documents
            if d['schema'].startswith(types.LAYERING_POLICY_SCHEMA)
        ]
        self.assertEqual(1, len(layering_policies))
        documents.remove(layering_policies[0])

        first_revision_ids = [
            d['status']['revision'] for d in documents
            if d['status']['bucket'] == 'first'
        ]
        second_revision_ids = [
            d['status']['revision'] for d in documents
            if d['status']['bucket'] == 'second'
        ]

        # Validate correct number of documents, the revision and bucket for
        # each document.
        self.assertEqual(4, len(documents))
        self.assertEqual(['first', 'first', 'second', 'second'],
                         [d['status']['bucket'] for d in documents])
        self.assertEqual(2, len(first_revision_ids))
        self.assertEqual(2, len(second_revision_ids))
        self.assertEqual([2, 2], first_revision_ids)
        self.assertEqual([4, 4], second_revision_ids)
示例#21
0
 def test_layering_without_layering_policy_raises_exc(self):
     doc_factory = factories.DocumentFactory(1, [1])
     documents = doc_factory.gen_test({}, site_abstract=False)[1:]
     self.assertRaises(errors.LayeringPolicyNotFound,
                       layering.DocumentLayering, documents)
示例#22
0
    def _test_list_rendered_documents(self, cleartext_secrets):
        """Validates that destination document that substitutes from an
        encrypted document is appropriately redacted when ``cleartext_secrets``
        is True.
        """
        rules = {
            'deckhand:list_cleartext_documents': '@',
            'deckhand:list_encrypted_documents': '@',
            'deckhand:create_cleartext_documents': '@',
            'deckhand:create_encrypted_documents': '@'
        }

        self.policy.set_rules(rules)

        doc_factory = factories.DocumentFactory(1, [1])

        layering_policy = doc_factory.gen_test({})[0]
        layering_policy['data']['layerOrder'] = ['global', 'site']
        certificate_data = 'sample-certificate'
        certificate_ref = ('http://127.0.0.1/key-manager/v1/secrets/%s' %
                           test_utils.rand_uuid_hex())
        redacted_data = dd.redact(certificate_ref)

        doc1 = {
            'data': certificate_data,
            'schema': 'deckhand/Certificate/v1',
            'name': 'example-cert',
            'layer': 'site',
            'metadata': {
                'schema': 'metadata/Document/v1',
                'name': 'example-cert',
                'layeringDefinition': {
                    'abstract': False,
                    'layer': 'site'
                },
                'storagePolicy': 'encrypted',
                'replacement': False
            }
        }

        original_substitutions = [{
            'dest': {
                'path': '.'
            },
            'src': {
                'schema': 'deckhand/Certificate/v1',
                'name': 'example-cert',
                'path': '.'
            }
        }]
        doc2 = {
            'data': {},
            'schema': 'example/Kind/v1',
            'name': 'deckhand-global',
            'layer': 'global',
            'metadata': {
                'labels': {
                    'global': 'global1'
                },
                'storagePolicy': 'cleartext',
                'layeringDefinition': {
                    'abstract': False,
                    'layer': 'global'
                },
                'name': 'deckhand-global',
                'schema': 'metadata/Document/v1',
                'substitutions': original_substitutions,
                'replacement': False
            }
        }

        payload = [layering_policy, doc1, doc2]

        # Create both documents and mock out SecretsManager.create to return
        # a fake Barbican ref.
        with mock.patch.object(  # noqa
                secrets_manager.SecretsManager,
                'create',
                return_value=certificate_ref):
            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']

        # Retrieve rendered documents and simulate a Barbican lookup by
        # causing the actual certificate data to be returned.
        with mock.patch.object(
                secrets_manager.SecretsManager,
                'get',  # noqa
                return_value=certificate_data):
            resp = self.app.simulate_get(
                '/api/v1.0/revisions/%s/rendered-documents' % revision_id,
                headers={'Content-Type': 'application/x-yaml'},
                params={
                    'metadata.name': ['example-cert', 'deckhand-global'],
                    'cleartext-secrets': str(cleartext_secrets)
                },
                params_csv=False)

        self.assertEqual(200, resp.status_code)
        rendered_documents = list(yaml.safe_load_all(resp.text))
        self.assertEqual(2, len(rendered_documents))

        if cleartext_secrets is True:
            # Expect the cleartext data to be returned.
            self.assertTrue(
                all(
                    map(lambda x: x['data'] == certificate_data,
                        rendered_documents)))
        else:
            # Expect redacted data for both documents to be returned -
            # because the destination document should receive redacted data.
            self.assertTrue(
                all(
                    map(lambda x: x['data'] == redacted_data,
                        rendered_documents)))
            destination_doc = next(
                iter(
                    filter(
                        lambda x: x['metadata']['name'] == 'deckhand-global',
                        rendered_documents)))
            substitutions = destination_doc['metadata']['substitutions']
            self.assertNotEqual(original_substitutions, substitutions)
示例#23
0
 def setUp(self):
     super(TestSecretsSubstitutionNegative, self).setUp()
     self.document_factory = factories.DocumentFactory(1, [1])
     self.secrets_factory = factories.DocumentSecretFactory()
示例#24
0
    def test_layering_two_parents_one_child_each_1(self):
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_GLOBAL_DATA_2_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_SITE_DATA_1_": {
                "data": {
                    "b": 4
                }
            },
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            },
            "_SITE_DATA_2_": {
                "data": {
                    "b": 3
                }
            },
            "_SITE_ACTIONS_2_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            }
        }
        doc_factory = factories.DocumentFactory(2, [2, 2])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         site_parent_selectors=[{
                                             'global':
                                             'global1'
                                         }, {
                                             'global':
                                             'global2'
                                         }])

        site_expected = [{
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 4
        }, {
            'a': {
                'x': 1,
                'y': 2
            },
            'b': 3
        }]
        self._test_layering(documents, site_expected)
示例#25
0
    def test_validation_data_schema_different_revision_expect_failure(self):
        """Validates that creating a ``DataSchema`` in one revision and then
        creating a document in another revision that relies on the previously
        created ``DataSchema`` results in an expected failure.
        """
        rules = {
            'deckhand:create_cleartext_documents': '@',
            'deckhand:list_validations': '@',
            'deckhand:list_cleartext_documents': '@',
            'deckhand:list_encrypted_documents': '@'
        }
        self.policy.set_rules(rules)

        # Create a `DataSchema` against which the test document will be
        # validated.
        data_schema_factory = factories.DataSchemaFactory()
        metadata_name = 'example/foo/v1'
        schema_to_use = {
            '$schema': 'http://json-schema.org/schema#',
            'type': 'object',
            'properties': {
                'a': {
                    'type': 'integer'  # Test doc will fail b/c of wrong type.
                }
            },
            'required': ['a']
        }
        data_schema = data_schema_factory.gen_test(metadata_name,
                                                   data=schema_to_use)
        revision_id = self._create_revision(payload=[data_schema])

        # Validate that the internal deckhand validation was created.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            1,
            'results': [{
                'name': types.DECKHAND_SCHEMA_VALIDATION,
                'status': 'success'
            }]
        }
        self.assertEqual(expected_body, body)

        # Create the test document that fails the validation due to the
        # schema defined by the `DataSchema` document.
        doc_factory = factories.DocumentFactory(1, [1])
        docs_to_test = doc_factory.gen_test(
            {'_GLOBAL_DATA_1_': {
                'data': {
                    'a': 'fail'
                }
            }},
            global_abstract=False)
        docs_to_test[1]['schema'] = 'example/foo/v1'
        docs_to_test[1]['metadata']['name'] = 'test_doc'

        revision_id = self._create_revision(payload=docs_to_test +
                                            [data_schema])

        # Validate that the validation was created and reports failure.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            1,
            'results': [{
                'name': types.DECKHAND_SCHEMA_VALIDATION,
                'status': 'failure'
            }]
        }
        self.assertEqual(expected_body, body)

        # Validate that the validation was created and reports failure.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/rendered-documents' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(400, resp.status_code)
    def test_substitution_without_parent_document(self):
        """Validate that a document with no parent undergoes substitution.

        global -> do nothing
        site -> (no parent & no children) requires substitution
        """
        mapping = {
            "_GLOBAL_DATA_1_": {
                "data": {
                    "a": {
                        "x": 1,
                        "y": 2
                    }
                }
            },
            "_SITE_DATA_1_": {
                "data": {
                    "b": 4
                }
            },
            "_SITE_SUBSTITUTIONS_1_": [{
                "dest": {
                    "path": ".c"
                },
                "src": {
                    "schema": "deckhand/Certificate/v1",
                    "name": "site-cert",
                    "path": "."
                }
            }],
            # No layering should be applied as the document has no parent.
            "_SITE_ACTIONS_1_": {
                "actions": [{
                    "method": "merge",
                    "path": "."
                }]
            }
        }
        doc_factory = factories.DocumentFactory(2, [1, 1])
        documents = doc_factory.gen_test(mapping,
                                         site_abstract=False,
                                         global_abstract=False)

        # Remove the labels from the global document so that the site document
        # (the child) has no parent.
        documents[1]['metadata']['labels'] = {}

        secrets_factory = factories.DocumentSecretFactory()
        certificate = secrets_factory.gen_test('Certificate',
                                               'cleartext',
                                               data='site-secret',
                                               name='site-cert')
        documents.append(certificate)

        global_expected = {'a': {'x': 1, 'y': 2}}
        site_expected = {'b': 4, 'c': 'site-secret'}

        self._test_layering(documents,
                            site_expected=site_expected,
                            global_expected=global_expected,
                            strict=False)
示例#27
0
    def test_validation_with_registered_data_schema_expect_mixed(self):
        rules = {
            'deckhand:create_cleartext_documents': '@',
            'deckhand:list_validations': '@',
            'deckhand:show_validation': '@'
        }
        self.policy.set_rules(rules)

        # Create a `DataSchema` against which the test document will be
        # validated.
        data_schema_factory = factories.DataSchemaFactory()
        metadata_name = 'example/foo/v1'
        schema_to_use = {
            '$schema': 'http://json-schema.org/schema#',
            'type': 'object',
            'properties': {
                'a': {
                    'type': 'integer'  # Test doc will fail b/c of wrong type.
                }
            },
            'required': ['a']
        }
        expected_errors = [{
            'error_section': {
                'a': 'fail'
            },
            'name': 'test_doc',
            'path': '.data.a',
            'schema': 'example/foo/v1',
            'message': "'fail' is not of type 'integer'",
            'validation_schema': schema_to_use,
            'schema_path': '.properties.a.type'
        }]
        data_schema = data_schema_factory.gen_test(metadata_name,
                                                   data=schema_to_use)

        # Create a document that passes validation and another that fails it.
        doc_factory = factories.DocumentFactory(1, [1])
        fail_doc = doc_factory.gen_test(
            {'_GLOBAL_DATA_1_': {
                'data': {
                    'a': 'fail'
                }
            }},
            global_abstract=False)[-1]
        fail_doc['schema'] = 'example/foo/v1'
        fail_doc['metadata']['name'] = 'test_doc'

        pass_doc = copy.deepcopy(fail_doc)
        pass_doc['data']['a'] = 5

        revision_id = self._create_revision(
            payload=[fail_doc, pass_doc, data_schema])

        # Validate that the validation reports failure since `fail_doc`
        # should've failed validation.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            1,
            'results': [{
                'name': types.DECKHAND_SCHEMA_VALIDATION,
                'status': 'failure'
            }]
        }
        self.assertEqual(expected_body, body)

        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations/%s' %
            (revision_id, types.DECKHAND_SCHEMA_VALIDATION),
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            3,
            'results': [
                {
                    'id': 0,
                    'status': 'failure'
                },  # fail_doc failed.
                {
                    'id': 1,
                    'status': 'success'
                },  # DataSchema passed.
                {
                    'id': 2,
                    'status': 'success'
                }
            ]  # pass_doc succeeded.
        }
        self.assertEqual(expected_body, body)

        # Validate that fail_doc validation failed for the expected reason.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations/%s/entries/0' %
            (revision_id, types.DECKHAND_SCHEMA_VALIDATION),
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_errors = [{
            'error_section': {
                'a': 'fail'
            },
            'name': 'test_doc',
            'path': '.data.a',
            'schema': 'example/foo/v1',
            'message': "'fail' is not of type 'integer'",
            'validation_schema': schema_to_use,
            'schema_path': '.properties.a.type'
        }]

        self.assertIn('errors', body)
        self.assertEqual(expected_errors, body['errors'])
示例#28
0
    def test_rendered_documents_fail_post_validation(self):
        """Validates that when fully rendered documents fail schema validation,
        a 400 is raised.

        For this scenario a DataSchema checks that the relevant document has
        a key in its data section, a key which is removed during the rendering
        process as the document uses a delete action. This triggers
        post-rendering validation failure.
        """
        rules = {
            'deckhand:list_cleartext_documents': '@',
            'deckhand:list_encrypted_documents': '@',
            'deckhand:create_cleartext_documents': '@'
        }
        self.policy.set_rules(rules)

        # Create a document for a bucket.
        documents_factory = factories.DocumentFactory(2, [1, 1])
        payload = documents_factory.gen_test(
            {
                "_GLOBAL_DATA_1_": {
                    "data": {
                        "a": "b"
                    }
                },
                "_SITE_DATA_1_": {
                    "data": {
                        "a": "b"
                    }
                },
                "_SITE_ACTIONS_1_": {
                    "actions": [{
                        "method": "delete",
                        "path": "."
                    }]
                }
            },
            site_abstract=False)

        data_schema_factory = factories.DataSchemaFactory()
        metadata_name = payload[-1]['schema']
        schema_to_use = {
            '$schema': 'http://json-schema.org/schema#',
            'type': 'object',
            'properties': {
                'a': {
                    'type': 'string'
                }
            },
            'required': ['a'],
            'additionalProperties': False
        }
        data_schema = data_schema_factory.gen_test(metadata_name,
                                                   data=schema_to_use)
        payload.append(data_schema)

        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']

        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/rendered-documents' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})

        self.assertEqual(400, resp.status_code)
示例#29
0
    def test_validation_only_new_data_schema_registered(self):
        """Validate whether newly created DataSchemas replace old DataSchemas
        when it comes to validation.
        """
        rules = {
            'deckhand:create_cleartext_documents': '@',
            'deckhand:list_validations': '@'
        }
        self.policy.set_rules(rules)

        # Create 2 DataSchemas that will fail if they're used. These shouldn't
        # be used for validation.
        data_schema_factory = factories.DataSchemaFactory()
        metadata_names = ['exampleA/Doc/v1', 'exampleB/Doc/v1']
        schemas_to_use = [{
            '$schema': 'http://json-schema.org/schema#',
            'type': 'object',
            'properties': {
                'a': {
                    'type': 'integer'
                }
            },
            'required': ['a'],
            'additionalProperties': False
        }] * 2
        old_data_schemas = [
            data_schema_factory.gen_test(metadata_names[i],
                                         data=schemas_to_use[i])
            for i in range(2)
        ]
        # Save the DataSchemas in the first revision.
        revision_id = self._create_revision(payload=old_data_schemas)

        # Create 2 DataSchemas that will pass if they're used. These should
        # be used for validation.
        for schema_to_use in schemas_to_use:
            schema_to_use['properties']['a']['type'] = 'string'
        new_data_schemas = [
            data_schema_factory.gen_test(metadata_names[i],
                                         data=schemas_to_use[i])
            for i in range(2)
        ]
        doc_factory = factories.DocumentFactory(1, [1])
        example1_doc = doc_factory.gen_test(
            {'_GLOBAL_DATA_1_': {
                'data': {
                    'a': 'whatever'
                }
            }},
            global_abstract=False)[-1]
        example1_doc['schema'] = metadata_names[0]
        example2_doc = copy.deepcopy(example1_doc)
        example2_doc['schema'] = metadata_names[1]
        # Save the documents that will be validated alongside the DataSchemas
        # that will be used to validate them.
        revision_id = self._create_revision(
            payload=[example1_doc, example2_doc] + new_data_schemas)

        # Validate that the validation was created and succeeded: This means
        # that the new DataSchemas were used, not the old ones.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count':
            1,
            'results': [{
                'name': types.DECKHAND_SCHEMA_VALIDATION,
                'status': 'success'
            }]
        }
        self.assertEqual(expected_body, body)
示例#30
0
    def test_validation_with_registered_data_schema(self):
        rules = {'deckhand:create_cleartext_documents': '@',
                 'deckhand:list_validations': '@'}
        self.policy.set_rules(rules)

        # Register a `DataSchema` against which the test document will be
        # validated.
        data_schema_factory = factories.DataSchemaFactory()
        metadata_name = 'example/Doc/v1'
        schema_to_use = {
            '$schema': 'http://json-schema.org/schema#',
            'type': 'object',
            'properties': {
                'a': {
                    'type': 'string'
                }
            },
            'required': ['a'],
            'additionalProperties': False
        }
        data_schema = data_schema_factory.gen_test(
            metadata_name, data=schema_to_use)

        revision_id = self._create_revision(payload=[data_schema])

        # Validate that the internal deckhand validation was created.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count': 1,
            'results': [
                {'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success'}
            ]
        }
        self.assertEqual(expected_body, body)

        # Create the test document whose data section adheres to the
        # `DataSchema` above.
        doc_factory = factories.DocumentFactory(1, [1])
        doc_to_test = doc_factory.gen_test(
            {'_GLOBAL_DATA_1_': {'data': {'a': 'whatever'}}},
            global_abstract=False)[-1]
        doc_to_test['schema'] = 'example/Doc/v1'

        revision_id = self._create_revision(
            payload=[doc_to_test])

        # Validate that the validation was created and passed.
        resp = self.app.simulate_get(
            '/api/v1.0/revisions/%s/validations' % revision_id,
            headers={'Content-Type': 'application/x-yaml'})
        self.assertEqual(200, resp.status_code)
        body = yaml.safe_load(resp.text)
        expected_body = {
            'count': 1,
            'results': [
                {'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success'}
            ]
        }
        self.assertEqual(expected_body, body)