def test_validation_document_duplication(self): """Validate that duplicate document fails when duplicate passed in.""" test_document = self._read_data('sample_document') # Should only fail when pre_validate is True as the `db` module already # handles this on behalf of the controller. validations = document_validation.DocumentValidation( [test_document] * 2, # Provide 2 of the same document. pre_validate=True).validate_all() expected_error = { 'diagnostic': mock.ANY, 'documents': [{ 'layer': test_document['metadata']['layeringDefinition'][ 'layer'], 'name': test_document['metadata']['name'], 'schema': test_document['schema'] }], 'error': True, 'kind': 'ValidationMessage', 'level': 'Error', 'message': 'Duplicate document exists', 'name': 'Deckhand validation error' } self.assertEqual(1, len(validations[1]['errors'])) self.assertEqual(expected_error, validations[1]['errors'][0]) # With pre_validate=False the validation should skip. validations = document_validation.DocumentValidation( [test_document] * 2, # Provide 2 of the same document. pre_validate=False).validate_all() self.assertEmpty(validations[1]['errors'])
def test_invalid_validation_schema_raises_runtime_error(self): document = self._read_data('sample_passphrase') fake_schema = mock.MagicMock(schema='fake') fake_schema_map = {'v1': {'deckhand/Passphrase': fake_schema}} # Validate that broken built-in base schema raises RuntimeError. with mock.patch.object( document_validation, 'base_schema', new_callable=mock.PropertyMock(return_value=fake_schema)): doc_validator = document_validation.DocumentValidation(document) with self.assertRaisesRegexp(RuntimeError, 'Unknown error'): doc_validator.validate_all() # Validate that broken built-in schema for ``SchemaValidator`` raises # RuntimeError. with mock.patch.object( document_validation.SchemaValidator, '_schema_map', new_callable=mock.PropertyMock(return_value=fake_schema_map)): doc_validator = document_validation.DocumentValidation(document) with self.assertRaisesRegexp(RuntimeError, 'Unknown error'): doc_validator.validate_all() # Validate that broken data schema for ``DataSchemaValidator`` raises # RuntimeError. document = self._read_data('sample_document') data_schema = self._read_data('sample_data_schema') data_schema['metadata']['name'] = document['schema'] data_schema['data'] = 'fake' doc_validator = document_validation.DocumentValidation( [document, data_schema]) with self.assertRaisesRegexp(RuntimeError, 'Unknown error'): doc_validator.validate_all()
def test_validation_failure_sanitizes_message_secrets(self): data_schema_factory = factories.DataSchemaFactory() metadata_name = 'example/Doc/v1' schema_to_use = { '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'secret-a': {'type': 'string'} }, 'required': ['secret-a'], 'additionalProperties': False } data_schema = data_schema_factory.gen_test( metadata_name, data=schema_to_use) # Case 1: Check that sensitive data is sanitized if the document has # substitutions and `metadata.storagePolicy` == 'cleartext'. document_factory = factories.DocumentFactory(1, [1]) test_document = document_factory.gen_test({ "_GLOBAL_DATA_1_": {'data': {'secret-a': 5}}, "_GLOBAL_SCHEMA_1_": metadata_name, "_GLOBAL_SUBSTITUTIONS_1_": [{ "dest": { "path": ".secret-a" }, "src": { "schema": "deckhand/CertificateKey/v1", "name": "site-cert", "path": "." } }], }, global_abstract=False)[-1] test_document['metadata']['storagePolicy'] = 'cleartext' validations = document_validation.DocumentValidation( test_document, existing_data_schemas=[data_schema], pre_validate=False).validate_all() self.assertEqual(1, len(validations[0]['errors'])) self.assertEqual('Sanitized to avoid exposing secret.', validations[0]['errors'][0]['message']) # Case 2: Check that sensitive data is sanitized if the document has # no substitutions and `metadata.storagePolicy` == 'encrypted'. test_document = document_factory.gen_test({ "_GLOBAL_DATA_1_": {'data': {'secret-a': 5}}, "_GLOBAL_SCHEMA_1_": metadata_name, "_GLOBAL_SUBSTITUTIONS_1_": [], }, global_abstract=False)[-1] test_document['metadata']['storagePolicy'] = 'encrypted' validations = document_validation.DocumentValidation( test_document, existing_data_schemas=[data_schema], pre_validate=False).validate_all() self.assertEqual(1, len(validations[0]['errors'])) self.assertEqual('Sanitized to avoid exposing secret.', validations[0]['errors'][0]['message'])
def _test_missing_required_sections(self, document, properties_to_remove): if document['metadata']['schema'].startswith(types.CONTROL): critial_properties = self.CRITICAL_CONTROL_PROPERTIES elif document['metadata']['schema'].startswith(types.DOCUMENT): critial_properties = self.CRITICAL_DOCUMENT_PROPERTIES else: self.fail('Document `metadata.schema` must start with ' '"metadata/Document" or "metadata/Control".') for idx, property_to_remove in enumerate(properties_to_remove): missing_prop = property_to_remove.split('.')[-1] invalid_data = self._corrupt_data(document, property_to_remove) exception_raised = property_to_remove in critial_properties expected_err_msg = "'%s' is a required property" % missing_prop payload = [invalid_data] doc_validator = document_validation.DocumentValidation( payload, pre_validate=False) if exception_raised: self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all) else: self._do_validations(doc_validator, invalid_data, expected_err_msg)
def _pre_validate_documents(self, documents): LOG.debug('%s performing document pre-validation.', self.__class__.__name__) validator = document_validation.DocumentValidation( documents, pre_validate=True) results = validator.validate_all() error_list = [] for result in results: for e in result['errors']: for d in e['documents']: LOG.error('Document [%s, %s] %s failed with ' 'pre-validation error: "%s". Diagnostic: "%s".', d['schema'], d['layer'], d['name'], e['message'], e['diagnostic']) error_list.append( ValidationMessage( message=e['message'], doc_schema=d['schema'], doc_name=d['name'], doc_layer=d['layer'])) if error_list: raise errors.InvalidDocumentFormat(error_list=error_list)
def _test_missing_required_sections(self, properties_to_remove): for idx, property_to_remove in enumerate(properties_to_remove): critical = property_to_remove in self.CRITICAL_ATTRS missing_prop = property_to_remove.split('.')[-1] invalid_data = self._corrupt_data(property_to_remove) expected_err = self.SCHEMA_ERR % missing_prop doc_validator = document_validation.DocumentValidation( invalid_data) if critical: self.assertRaisesRegexp( errors.InvalidDocumentFormat, expected_err, doc_validator.validate_all) else: validations = doc_validator.validate_all() self.assertEqual(1, len(validations)) self.assertEqual('failure', validations[0]['status']) self.assertEqual({'version': '1.0', 'name': 'deckhand'}, validations[0]['validator']) self.assertEqual(types.DECKHAND_SCHEMA_VALIDATION, validations[0]['name']) self.assertEqual(1, len(validations[0]['errors'])) self.assertEqual(self.data['metadata']['name'], validations[0]['errors'][0]['name']) self.assertEqual(self.data['schema'], validations[0]['errors'][0]['schema']) self.assertEqual(expected_err, validations[0]['errors'][0]['message'])
def test_generic_document_missing_multiple_required_sections(self): """Validates that multiple errors are reported for a document with multiple validation errors. """ document = self._read_data('sample_document') properties_to_remove = ( 'metadata.layeringDefinition.actions.0.method', 'metadata.layeringDefinition.actions.0.path', 'metadata.substitutions.0.dest.path', 'metadata.substitutions.0.src.name', 'metadata.substitutions.0.src.path', 'metadata.substitutions.0.src.schema', ) for property_to_remove in properties_to_remove: document = self._corrupt_data(document, property_to_remove) doc_validator = document_validation.DocumentValidation(document) e = self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all) for idx, property_to_remove in enumerate(properties_to_remove): parts = property_to_remove.split('.') missing_property = parts[-1] error_re = r"%s is a required property" % missing_property self.assertRegex(str(e.error_list).replace("\'", ""), error_re)
def on_put(self, req, resp, bucket_name=None): data = self.from_yaml(req, expect_list=True, allow_empty=True) documents = document_wrapper.DocumentDict.from_list(data) # NOTE: Must validate documents before doing policy enforcement, # because we expect certain formatting of the documents while doing # policy enforcement. If any documents fail basic schema validaiton # raise an exception immediately. data_schemas = db_api.revision_documents_get( schema=types.DATA_SCHEMA_SCHEMA, deleted=False) try: doc_validator = document_validation.DocumentValidation( documents, data_schemas, pre_validate=True) validations = doc_validator.validate_all() except deckhand_errors.InvalidDocumentFormat as e: with excutils.save_and_reraise_exception(): LOG.exception(e.format_message()) for document in documents: if secrets_manager.SecretsManager.requires_encryption(document): policy.conditional_authorize( 'deckhand:create_encrypted_documents', req.context) break documents = self._encrypt_secret_documents(documents) created_documents = self._create_revision_documents( bucket_name, documents) if created_documents: revision_id = created_documents[0]['revision_id'] self._create_revision_validations(revision_id, validations) resp.body = self.view_builder.list(created_documents) resp.status = falcon.HTTP_200
def deckhand_render(documents=None, fail_on_missing_sub_src=False, validate=True): documents = documents or [] errors = [] rendered_documents = [] schemas, schema_errors = load_schemas_from_docs(documents) errors.extend(schema_errors) try: deckhand_eng = layering.DocumentLayering( documents, fail_on_missing_sub_src=fail_on_missing_sub_src, validate=validate) rendered_documents = [dict(d) for d in deckhand_eng.render()] if validate: validator = document_validation.DocumentValidation( rendered_documents) results = validator.validate_all() for result in results: if result['errors']: errors.append( (DECKHAND_RENDER_EXCEPTION, 'During rendering Deckhand was unable to validate ' 'the following document, details: %s.' % (result['errors']))) except dh_errors.DeckhandException as e: errors.append((DECKHAND_RENDER_EXCEPTION, 'An unknown Deckhand exception occurred while trying' ' to render documents: %s. Details: %s.' % (str(e), e.error_list))) return rendered_documents, errors
def test_validation_failure_sanitizes_error_section_secrets( self, mock_jsonschema): m_args = mock.Mock() mock_jsonschema.Draft4Validator(m_args).iter_errors.side_effect = [ # Return empty list of errors for base schema and metadata # validator and pretend that 1 error is returned for next # validator. [], [], [mock.Mock(path=[], schema_path=[], message='scary-secret-here')] ] document_factory = factories.DocumentFactory(1, [1]) test_document = document_factory.gen_test( { '_GLOBAL_DATA_1_': {'data': {'secret-a': 5}}, '_GLOBAL_SUBSTITUTIONS_1_': [ {'src': { 'path': '.', 'schema': 'foo/bar/v1', 'name': 'foo'}, 'dest': {'path': '.secret-a'}} ] }, global_abstract=False)[-1] data_schema_factory = factories.DataSchemaFactory() data_schema = data_schema_factory.gen_test(test_document['schema'], {}) validations = document_validation.DocumentValidation( test_document, existing_data_schemas=[data_schema], pre_validate=False).validate_all() self.assertEqual(1, len(validations[0]['errors'])) self.assertIn('Sanitized to avoid exposing secret.', str(validations[0]['errors'][-1])) self.assertNotIn('scary-secret.', str(validations[0]['errors'][-1]))
def test_data_schema_missing_optional_sections(self): optional_missing_data = [ self._corrupt_data(self.test_document, 'metadata.labels'), ] for missing_data in optional_missing_data: payload = [missing_data, self.dataschema] document_validation.DocumentValidation(payload).validate_all()
def test_actions_but_no_parent_selector_raises_validation_error(self): # Verify that an error is thrown if actions are specified but # parentSelector is missing altogether. document = self._read_data('sample_document') document['metadata']['layeringDefinition'].pop('parentSelector') doc_validator = document_validation.DocumentValidation( [document], pre_validate=False) self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all) # Verify that an error is thrown if actions are specified but no # parentSelector labels are. document['metadata']['layeringDefinition']['parentSelector'] = {} doc_validator = document_validation.DocumentValidation( [document], pre_validate=False) self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all)
def test_data_schema_missing_optional_sections(self): self._read_data('sample_data_schema') optional_missing_data = [ self._corrupt_data('metadata.labels'), ] for missing_data in optional_missing_data: document_validation.DocumentValidation(missing_data).validate_all()
def test_invalid_document_schema_generates_error(self, mock_log): document = self._read_data('sample_document') document['schema'] = 'foo/bar/v1' doc_validator = document_validation.DocumentValidation(document) doc_validator.validate_all() self.assertRegex( mock_log.error.mock_calls[0][1][0], 'The provided document schema %s is invalid.' % document['schema'])
def test_invalid_document_schema_version_generates_error(self, mock_log): document = self._read_data('sample_passphrase') document['schema'] = 'deckhand/Passphrase/v5' doc_validator = document_validation.DocumentValidation(document) doc_validator.validate_all() self.assertRegex( mock_log.error.mock_calls[0][1][0], 'The provided document schema %s is invalid.' % document['schema'])
def test_invalid_validation_schema_raises_runtime_error(self): document = self._read_data('sample_passphrase') # Validate that broken built-in base schema raises RuntimeError. doc_validator = document_validation.DocumentValidation(document) doc_validator._validators[0].base_schema = 'fake' with self.assertRaisesRegexp(RuntimeError, 'Unknown error'): doc_validator.validate_all() # Validate that broken data schema for ``DataSchemaValidator`` raises # RuntimeError. document = self._read_data('sample_document') data_schema = self._read_data('sample_data_schema') data_schema['metadata']['name'] = document['schema'] data_schema['data'] = 'fake' doc_validator = document_validation.DocumentValidation( [document, data_schema], pre_validate=False) with self.assertRaisesRegexp(RuntimeError, 'Unknown error'): doc_validator.validate_all()
def test_document_missing_optional_sections(self): properties_to_remove = ( 'metadata.substitutions', 'metadata.substitutions.2.dest.pattern') for property_to_remove in properties_to_remove: missing_data = self._corrupt_data(self.test_document, property_to_remove) payload = [missing_data, self.dataschema] document_validation.DocumentValidation(payload).validate_all()
def test_parent_selector_and_actions_both_provided_is_valid(self): test_document = self._read_data('sample_document') data_schema_factory = factories.DataSchemaFactory() data_schema = data_schema_factory.gen_test(test_document['schema'], {}) validations = document_validation.DocumentValidation( test_document, existing_data_schemas=[data_schema], pre_validate=False).validate_all() self.assertEmpty(validations[0]['errors'])
def test_abstract_document_not_validated(self, mock_log): test_document = self._read_data('sample_passphrase') # Set the document to abstract. abstract_document = utils.jsonpath_replace( test_document, True, '.metadata.layeringDefinition.abstract') document_validation.DocumentValidation( abstract_document).validate_all() self.assertTrue(mock_log.info.called) self.assertIn("Skipping schema validation for abstract document", mock_log.info.mock_calls[0][1][0])
def test_parent_selector_but_no_actions_raises_validation_error(self): # Verify that an error is thrown if parentSelector is specified but # actions is missing altogether. document = self._read_data('sample_document') document['metadata']['layeringDefinition']['parentSelector'] = { 'some': 'label' } document['metadata']['layeringDefinition'].pop('actions') doc_validator = document_validation.DocumentValidation( [document], pre_validate=False) self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all) # Verify that an error is thrown if parentSelector is specified but # at least 1 action isn't specified. document['metadata']['layeringDefinition']['actions'] = [] doc_validator = document_validation.DocumentValidation( [document], pre_validate=False) self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all)
def test_document_missing_optional_sections(self): self._read_data('sample_document') properties_to_remove = ('metadata.layeringDefinition.actions', 'metadata.layeringDefinition.parentSelector', 'metadata.substitutions', 'metadata.substitutions.2.dest.pattern') for property_to_remove in properties_to_remove: optional_data_removed = self._corrupt_data(property_to_remove) document_validation.DocumentValidation( optional_data_removed).validate_all()
def on_get(self, req, resp, revision_id): include_encrypted = policy.conditional_authorize( 'deckhand:list_encrypted_documents', req.context, do_raise=False) filters = { 'metadata.storagePolicy': ['cleartext'], 'deleted': False } if include_encrypted: filters['metadata.storagePolicy'].append('encrypted') cleartext_secrets = req.get_param_as_bool('cleartext-secrets') if cleartext_secrets is None: cleartext_secrets = True req.params.pop('cleartext-secrets', None) rendered_documents, cache_hit = common.get_rendered_docs( revision_id, cleartext_secrets, **filters) # If the rendered documents result set is cached, then post-validation # for that result set has already been performed successfully, so it # can be safely skipped over as an optimization. if not cache_hit: data_schemas = db_api.revision_documents_get( schema=types.DATA_SCHEMA_SCHEMA, deleted=False) validator = document_validation.DocumentValidation( rendered_documents, data_schemas, pre_validate=False) engine.validate_render(revision_id, rendered_documents, validator) # Filters to be applied post-rendering, because many documents are # involved in rendering. User filters can only be applied once all # documents have been rendered. Note that `layering` module only # returns concrete documents, so no filtering for that is needed here. order_by = req.params.pop('order', None) sort_by = req.params.pop('sort', None) limit = req.params.pop('limit', None) user_filters = req.params.copy() if not cleartext_secrets: rendered_documents = utils.redact_documents(rendered_documents) rendered_documents = [ d for d in rendered_documents if utils.deepfilter( d, **user_filters)] if sort_by: rendered_documents = utils.multisort( rendered_documents, sort_by, order_by) if limit is not None: rendered_documents = rendered_documents[:limit] resp.status = falcon.HTTP_200 resp.body = self.view_builder.list(rendered_documents)
def test_abstract_document_not_validated(self, mock_log): self._read_data('sample_document') # Set the document to abstract. updated_data = self._corrupt_data( 'metadata.layeringDefinition.abstract', True, op='replace') # Guarantee that a validation error is thrown by removing a required # property. del updated_data['metadata']['layeringDefinition']['layer'] document_validation.DocumentValidation(updated_data).validate_all() self.assertTrue(mock_log.info.called) self.assertIn("Skipping schema validation for abstract document", mock_log.info.mock_calls[0][1][0])
def test_neither_parent_selector_nor_actions_provided_is_valid(self): test_document = self._read_data('sample_document') test_document['metadata']['layeringDefinition'].pop('actions') test_document['metadata']['layeringDefinition'].pop('parentSelector') data_schema_factory = factories.DataSchemaFactory() data_schema = data_schema_factory.gen_test(test_document['schema'], {}) validations = document_validation.DocumentValidation( test_document, existing_data_schemas=[data_schema], pre_validate=False).validate_all() self.assertEmpty(validations[0]['errors'])
def test_document_invalid_layering_definition_action(self): document = self._read_data('sample_document') missing_data = self._corrupt_data( document, 'metadata.layeringDefinition.actions.0.method', 'invalid', op='replace') error_re = (r".*invalid is not one of \[replace, delete, merge\]") payload = [missing_data] doc_validator = document_validation.DocumentValidation(payload) e = self.assertRaises(errors.InvalidDocumentFormat, doc_validator.validate_all) self.assertRegex(str(e.error_list[0]).replace("\'", ""), error_re)
def _post_validate(self, documents): # Perform schema validation post-rendering to ensure that rendering # and substitution didn't break anything. data_schemas = db_api.revision_documents_get( schema=types.DATA_SCHEMA_SCHEMA, deleted=False) doc_validator = document_validation.DocumentValidation( documents, data_schemas) try: doc_validator.validate_all() except errors.InvalidDocumentFormat as e: LOG.error('Failed to post-validate rendered documents.') LOG.exception(e.format_message()) raise falcon.HTTPInternalServerError( description=e.format_message())
def test_document_invalid_layering_definition_action(self): document = self._read_data('sample_document') missing_data = self._corrupt_data( document, 'metadata.layeringDefinition.actions.0.method', 'invalid', op='replace') expected_err = "'invalid' is not one of ['replace', 'delete', 'merge']" # Ensure that a dataschema document exists for the random document # schema via mocking. dataschema_factory = factories.DataSchemaFactory() dataschema = dataschema_factory.gen_test(document['schema'], {}) payload = [dataschema, missing_data] doc_validator = document_validation.DocumentValidation(payload) self._do_validations(doc_validator, document, expected_err)
def _validate_documents(self, documents): LOG.debug('%s performing document pre-validation.', self.__class__.__name__) validator = document_validation.DocumentValidation(documents, pre_validate=True) results = validator.validate_all() val_errors = [] for result in results: val_errors.extend([(e['schema'], e['name'], e['message']) for e in result['errors']]) if val_errors: for error in val_errors: LOG.error( 'Document [%s] %s failed with pre-validation error: %s.', *error) raise errors.InvalidDocumentFormat( details='The following pre-validation errors occurred ' '(schema, name, error): %s.' % val_errors)
def _post_validate(self, rendered_documents): # Perform schema validation post-rendering to ensure that rendering # and substitution didn't break anything. data_schemas = db_api.revision_documents_get( schema=types.DATA_SCHEMA_SCHEMA, deleted=False) doc_validator = document_validation.DocumentValidation( rendered_documents, data_schemas, pre_validate=False) try: validations = doc_validator.validate_all() except errors.InvalidDocumentFormat as e: with excutils.save_and_reraise_exception(): # Post-rendering validation errors likely indicate an internal # rendering bug, so override the default code to 500. e.code = 500 LOG.error('Failed to post-validate rendered documents.') LOG.exception(e.format_message()) else: error_list = [] for validation in validations: if validation['status'] == 'failure': error_list.extend([ vm.ValidationMessage( message=error['message'], name=vm.DOCUMENT_POST_RENDERING_FAILURE, doc_schema=error['schema'], doc_name=error['name'], doc_layer=error['layer'], diagnostic={ k: v for k, v in error.items() if k in ( 'schema_path', 'validation_schema', 'error_section' ) } ) for error in validation['errors'] ]) if error_list: raise errors.InvalidDocumentFormat( error_list=error_list, reason='Validation' )
def _test_missing_required_sections(self, document, properties_to_remove): for idx, property_to_remove in enumerate(properties_to_remove): missing_prop = property_to_remove.split('.')[-1] invalid_data = self._corrupt_data(document, property_to_remove) exception_raised = self.exception_map.get(property_to_remove, None) expected_err_msg = "'%s' is a required property" % missing_prop dataschema_factory = factories.DataSchemaFactory() dataschema = dataschema_factory.gen_test( invalid_data.get('schema', ''), {}) payload = [dataschema, invalid_data] doc_validator = document_validation.DocumentValidation(payload) if exception_raised: self.assertRaises(exception_raised, doc_validator.validate_all) else: self._do_validations(doc_validator, invalid_data, expected_err_msg)