Exemple #1
0
def cleanup_preview_destination(event, resources):
    storage = event.request.registry.storage

    for impacted in event.impacted_records:
        old_collection = impacted["old"]

        resource, signer = pick_resource_and_signer(event.request, resources,
                                                    bucket_id=event.payload["bucket_id"],
                                                    collection_id=old_collection["id"])
        if resource is None:
            continue

        for k in ("preview", "destination"):
            if k not in resource:  # pragma: nocover
                continue
            bid = resource[k]["bucket"]
            cid = resource[k]["collection"]
            collection_uri = instance_uri(event.request, "collection", bucket_id=bid, id=cid)
            storage.delete_all("record", collection_uri, with_deleted=True)

            updater = LocalUpdater(signer=signer,
                                   storage=storage,
                                   permission=event.request.registry.permission,
                                   source=resource['source'],
                                   destination=resource[k])
            updater.sign_and_update_destination(event.request,
                                                source_attributes=old_collection,
                                                next_source_status=None)
Exemple #2
0
def set_work_in_progress_status(event, resources):
    """Put the status in work-in-progress if was signed.
    """
    resource, signer = pick_resource_and_signer(event.request, resources,
                                                bucket_id=event.payload["bucket_id"],
                                                collection_id=event.payload["collection_id"])
    # Skip if resource is not configured.
    if resource is None:
        return

    updater = LocalUpdater(signer=signer,
                           storage=event.request.registry.storage,
                           permission=event.request.registry.permission,
                           source=resource['source'],
                           destination=resource['destination'])
    updater.update_source_status(STATUS.WORK_IN_PROGRESS, event.request)
Exemple #3
0
 def test_updater_raises_if_resources_are_not_set_properly(self):
     with pytest.raises(ValueError) as excinfo:
         LocalUpdater(source={'bucket': 'source'},
                      destination={},
                      signer=self.signer_instance,
                      storage=self.storage,
                      permission=self.permission)
     assert str(excinfo.value) == ("Resources should contain both "
                                   "bucket and collection")
Exemple #4
0
def sign_collection_data(event, resources):
    """
    Listen to resource change events, to check if a new signature is
    requested.

    When a source collection specified in settings is modified, and its
    new metadata ``status`` is set to ``"to-sign"``, then sign the data
    and update the destination.
    """
    payload = event.payload

    current_user_id = event.request.prefixed_userid
    if current_user_id == _PLUGIN_USERID:
        # Ignore changes made by plugin.
        return

    for impacted in event.impacted_records:
        new_collection = impacted['new']

        key = instance_uri(event.request, "collection",
                           bucket_id=payload['bucket_id'],
                           id=new_collection['id'])
        resource = resources.get(key)

        # Only sign the configured resources.
        if resource is None:
            continue

        registry = event.request.registry
        updater = LocalUpdater(signer=registry.signers[key],
                               storage=registry.storage,
                               permission=registry.permission,
                               source=resource['source'],
                               destination=resource['destination'])

        try:
            new_status = new_collection.get("status")
            if new_status == STATUS.TO_SIGN:
                # Run signature process (will set `last_reviewer` field).
                updater.sign_and_update_destination(event.request)

            elif new_status == STATUS.TO_REVIEW:
                if 'preview' in resource:
                    # If preview collection: update and sign preview collection
                    updater.destination = resource['preview']
                    updater.sign_and_update_destination(event.request,
                                                        next_source_status=STATUS.TO_REVIEW)
                else:
                    # If no preview collection: just track `last_editor`
                    updater.update_source_editor(event.request)

        except Exception:
            logger.exception("Could not sign '{0}'".format(key))
            event.request.response.status = 503
Exemple #5
0
    def setUp(self):
        self.storage = mock.MagicMock()
        self.permission = mock.MagicMock()
        self.signer_instance = mock.MagicMock()
        self.updater = LocalUpdater(source={
            'bucket': 'sourcebucket',
            'collection': 'sourcecollection'
        },
                                    destination={
                                        'bucket': 'destbucket',
                                        'collection': 'destcollection'
                                    },
                                    signer=self.signer_instance,
                                    storage=self.storage,
                                    permission=self.permission)

        # Resource events are bypassed completely in this test suite.
        patcher = mock.patch('kinto_signer.updater.build_request')
        self.addCleanup(patcher.stop)
        patcher.start()
Exemple #6
0
def set_work_in_progress_status(event, resources):
    """Put the status in work-in-progress if was signed.
    """
    payload = event.payload

    key = instance_uri(event.request, "collection",
                       bucket_id=payload["bucket_id"],
                       id=payload["collection_id"])
    resource = resources.get(key)

    # Skip if resource is not configured.
    if resource is None:
        return

    registry = event.request.registry
    updater = LocalUpdater(signer=registry.signers[key],
                           storage=registry.storage,
                           permission=registry.permission,
                           source=resource['source'],
                           destination=resource['destination'])
    updater.update_source_status(STATUS.WORK_IN_PROGRESS, event.request)
Exemple #7
0
    def on_resource_changed(event):
        payload = event.payload
        requested_resource = "{bucket_id}/{collection_id}".format(**payload)
        if requested_resource not in available_resources:
            return  # Only sign the configured resources.

        resource = available_resources.get(requested_resource)
        should_sign = any([True for r in event.impacted_records
                           if r['new'].get('status') == 'to-sign'])
        if not should_sign:
            return  # Only sign when the new collection status is "to-sign".

        registry = event.request.registry
        updater = LocalUpdater(
            signer=registry.signer,
            storage=registry.storage,
            permission=registry.permission,
            source=resource['source'],
            destination=resource['destination'])

        updater.sign_and_update_remote()
Exemple #8
0
def set_work_in_progress_status(event, resources):
    """Put the status in work-in-progress if was signed.
    """
    payload = event.payload

    key = instance_uri(event.request,
                       "collection",
                       bucket_id=payload["bucket_id"],
                       id=payload["collection_id"])
    resource = resources.get(key)

    # Skip if resource is not configured.
    if resource is None:
        return

    registry = event.request.registry
    updater = LocalUpdater(signer=registry.signers[key],
                           storage=registry.storage,
                           permission=registry.permission,
                           source=resource['source'],
                           destination=resource['destination'])
    updater.update_source_status(STATUS.WORK_IN_PROGRESS, event.request)
    def setUp(self):
        self.storage = mock.MagicMock()
        self.permission = mock.MagicMock()
        self.signer_instance = mock.MagicMock()
        self.updater = LocalUpdater(
            source={
                'bucket': 'sourcebucket',
                'collection': 'sourcecollection'},
            destination={
                'bucket': 'destbucket',
                'collection': 'destcollection'},
            signer=self.signer_instance,
            storage=self.storage,
            permission=self.permission)

        # Resource events are bypassed completely in this test suite.
        patcher = mock.patch('kinto_signer.utils.build_request')
        self.addCleanup(patcher.stop)
        patcher.start()
Exemple #10
0
class LocalUpdaterTest(unittest.TestCase):

    def setUp(self):
        self.storage = mock.MagicMock()
        self.permission = mock.MagicMock()
        self.signer_instance = mock.MagicMock()
        self.updater = LocalUpdater(
            source={
                'bucket': 'sourcebucket',
                'collection': 'sourcecollection'},
            destination={
                'bucket': 'destbucket',
                'collection': 'destcollection'},
            signer=self.signer_instance,
            storage=self.storage,
            permission=self.permission)

        # Resource events are bypassed completely in this test suite.
        patcher = mock.patch('kinto_signer.utils.build_request')
        self.addCleanup(patcher.stop)
        patcher.start()

    def patch(self, obj, *args, **kwargs):
        patcher = mock.patch.object(obj, *args, **kwargs)
        self.addCleanup(patcher.stop)
        return patcher.start()

    def test_updater_raises_if_resources_are_not_set_properly(self):
        with pytest.raises(ValueError) as excinfo:
            LocalUpdater(
                source={'bucket': 'source'},
                destination={},
                signer=self.signer_instance,
                storage=self.storage,
                permission=self.permission)
        assert str(excinfo.value) == ("Resources should contain both "
                                      "bucket and collection")

    def test_get_source_records_asks_storage_for_records(self):
        self.storage.list_all.return_value = []

        self.updater.get_source_records(None)
        self.storage.list_all.assert_called_with(
            resource_name='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_get_source_records_asks_storage_for_last_modified_records(self):
        self.storage.list_all.return_value = []

        self.updater.get_source_records(1234)
        self.storage.list_all.assert_called_with(
            resource_name='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            filters=[Filter('last_modified', 1234, COMPARISON.GT)],
            sorting=[Sort('last_modified', 1)])

    def test_get_destination_records(self):
        # We want to test get_destination_records with some records.
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.storage.list_all.return_value = records
        self.updater.get_destination_records()
        self.storage.resource_timestamp.assert_called_with(
            resource_name='record',
            parent_id='/buckets/destbucket/collections/destcollection')
        self.storage.list_all.assert_called_with(
            resource_name='record',
            parent_id='/buckets/destbucket/collections/destcollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_push_records_to_destination(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        assert self.storage.update.call_count == 3

    def test_push_records_to_destination_raises_if_storage_is_misconfigured(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([{}], 1324))
        self.patch(self.updater, 'get_source_records',
                   return_value=([{}], 1234))
        with pytest.raises(ValueError):
            self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_to_destination_does_not_raises_if_collection_is_empty(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        self.patch(self.updater, 'get_source_records',
                   return_value=([], 1234))
        with pytest.raises(ValueError):
            self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_removes_deleted_records(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{'id': idx, 'deleted': True, 'last_modified': 42}
                        for idx in range(3, 5)])
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=1324)
        assert self.storage.update.call_count == 2
        assert self.storage.delete.call_count == 2

    def test_push_records_skip_already_deleted_records(self):
        # In case the record doesn't exists in the destination
        # a RecordNotFoundError is raised.
        self.storage.delete.side_effect = RecordNotFoundError()
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{'id': idx, 'deleted': True, 'last_modified': 42}
                       for idx in range(3, 5)])
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        # Calling the updater should not raise the RecordNotFoundError.
        self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_to_destination_with_no_destination_changes(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], None))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=None)
        assert self.storage.update.call_count == 3

    def test_set_destination_signature_modifies_the_destination_collection(self):
        self.storage.get.return_value = {'id': 1234, 'last_modified': 1234}
        self.updater.set_destination_signature(mock.sentinel.signature,
                                               {},
                                               DummyRequest())

        self.storage.update.assert_called_with(
            resource_name='collection',
            object_id='destcollection',
            parent_id='/buckets/destbucket',
            obj={
                'id': 1234,
                'signature': mock.sentinel.signature
            })

    def test_set_destination_signature_copies_kinto_admin_ui_fields(self):
        self.storage.get.return_value = {'id': 1234, 'sort': '-age', 'last_modified': 1234}
        self.updater.set_destination_signature(mock.sentinel.signature,
                                               {'displayFields': ['name'], 'sort': 'size'},
                                               DummyRequest())

        self.storage.update.assert_called_with(
            resource_name='collection',
            object_id='destcollection',
            parent_id='/buckets/destbucket',
            obj={
                'id': 1234,
                'signature': mock.sentinel.signature,
                'sort': '-age',
                'displayFields': ['name']
            })

    def test_update_source_status_modifies_the_source_collection(self):
        self.storage.get.return_value = {'id': 1234, 'last_modified': 1234,
                                         'status': 'to-sign'}

        with mock.patch("kinto_signer.updater.datetime") as mocked:
            mocked.datetime.now().isoformat.return_value = "2018-04-09"
            self.updater.update_source_status(STATUS.SIGNED, DummyRequest())

        self.storage.update.assert_called_with(
            resource_name='collection',
            object_id='sourcecollection',
            parent_id='/buckets/sourcebucket',
            obj={
                'id': 1234,
                'last_review_by': 'basicauth:bob',
                'last_review_date': '2018-04-09',
                'last_signature_by': 'basicauth:bob',
                'last_signature_date': '2018-04-09',
                'status': "signed"
            })

    def test_create_destination_updates_collection_permissions(self):
        collection_uri = '/buckets/destbucket/collections/destcollection'
        request = DummyRequest()
        request.route_path.return_value = collection_uri
        self.updater.create_destination(request)
        request.registry.permission.replace_object_permissions.assert_called_with(
            collection_uri,
            {"read": ("system.Everyone",)})

    def test_create_destination_creates_bucket(self):
        request = DummyRequest()
        self.updater.create_destination(request)
        request.registry.storage.create.assert_any_call(
            resource_name='bucket',
            parent_id='',
            obj={"id": 'destbucket'})

    def test_create_destination_creates_collection(self):
        bucket_id = '/buckets/destbucket'
        request = DummyRequest()
        self.updater.create_destination(request)
        request.registry.storage.create.assert_any_call(
            resource_name='collection',
            parent_id=bucket_id,
            obj={"id": 'destcollection'})

    def test_sign_and_update_destination(self):
        records = [{'id': idx, 'foo': 'bar %s' % idx, 'last_modified': idx}
                   for idx in range(1, 3)]
        self.storage.list_all.return_value = records

        self.patch(self.storage, 'update_records')
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], '0'))
        self.patch(self.updater, 'push_records_to_destination')
        self.patch(self.updater, 'set_destination_signature')
        self.patch(self.updater, 'invalidate_cloudfront_cache')

        self.updater.sign_and_update_destination(DummyRequest(), {'id': 'source'})

        assert self.updater.get_destination_records.call_count == 1
        assert self.updater.push_records_to_destination.call_count == 1
        assert self.updater.set_destination_signature.call_count == 1
        assert self.updater.invalidate_cloudfront_cache.call_count == 1

    def test_refresh_signature_does_not_push_records(self):
        self.storage.list_all.return_value = []
        self.patch(self.updater, 'set_destination_signature')
        self.patch(self.updater, 'push_records_to_destination')
        self.patch(self.updater, 'invalidate_cloudfront_cache')

        self.updater.refresh_signature(DummyRequest(), 'signed')

        assert self.updater.invalidate_cloudfront_cache.call_count == 1
        assert self.updater.set_destination_signature.call_count == 1
        assert self.updater.push_records_to_destination.call_count == 0

    def test_refresh_signature_restores_status_on_source(self):
        self.storage.list_all.return_value = []
        with mock.patch('kinto_signer.updater.datetime') as mocked:
            mocked.datetime.now.return_value = datetime.datetime(2010, 10, 31)

            self.updater.refresh_signature(DummyRequest(), 'work-in-progress')

        new_attrs = {
            'status': 'work-in-progress',
            'last_signature_by': 'basicauth:bob',
            'last_signature_date': '2010-10-31T00:00:00'
        }
        self.storage.update.assert_any_call(resource_name='collection',
                                            parent_id='/buckets/sourcebucket',
                                            object_id='sourcecollection',
                                            obj=new_attrs)

    def test_if_distribution_id_a_cloudfront_invalidation_request_is_triggered(self):
        request = mock.MagicMock()
        request.registry.settings = {'signer.distribution_id': 'DWIGHTENIS'}
        with mock.patch('boto3.client') as boto3_client:
            self.updater.invalidate_cloudfront_cache(request, 'tz_1234')
            call_args = boto3_client.return_value.create_invalidation.call_args
            params = call_args[1]
            assert params['DistributionId'] == "DWIGHTENIS"
            assert params['InvalidationBatch']['CallerReference'].startswith('tz_1234-')
            assert params['InvalidationBatch']['Paths'] == {
                'Quantity': 1,
                'Items': ['/v1/*']
            }

    def test_does_not_fail_when_cache_invalidation_does(self):
        request = mock.MagicMock()
        request.registry.settings = {'signer.distribution_id': 'DWIGHTENIS'}
        with mock.patch('boto3.client') as boto3_client:
            boto3_client.return_value.create_invalidation.side_effect = ValueError
            self.updater.invalidate_cloudfront_cache(request, 'tz_1234')

    def test_invalidation_paths_can_be_configured(self):
        request = mock.MagicMock()
        request.registry.settings = {
            'signer.distribution_id': 'DWIGHTENIS',
            'signer.invalidation_paths': '/v1/blocklists* '
                                         '/v1/buckets/{bucket_id}/collections/{collection_id}*'
        }
        with mock.patch('boto3.client') as boto3_client:
            self.updater.invalidate_cloudfront_cache(request, 'tz_1234')
            call_args = boto3_client.return_value.create_invalidation.call_args
            params = call_args[1]
            assert params['InvalidationBatch']['Paths']['Quantity'] == 2
            assert params['InvalidationBatch']['Paths']['Items'] == [
                '/v1/blocklists*',
                '/v1/buckets/destbucket/collections/destcollection*'
            ]
Exemple #11
0
class LocalUpdaterTest(unittest.TestCase):

    def setUp(self):
        self.storage = mock.MagicMock()
        self.permission = mock.MagicMock()
        self.signer_instance = mock.MagicMock()
        self.updater = LocalUpdater(
            source={
                'bucket': 'sourcebucket',
                'collection': 'sourcecollection'},
            destination={
                'bucket': 'destbucket',
                'collection': 'destcollection'},
            signer=self.signer_instance,
            storage=self.storage,
            permission=self.permission)

        # Resource events are bypassed completely in this test suite.
        patcher = mock.patch('kinto_signer.updater.build_request')
        self.addCleanup(patcher.stop)
        patcher.start()

    def patch(self, obj, *args, **kwargs):
        patcher = mock.patch.object(obj, *args, **kwargs)
        self.addCleanup(patcher.stop)
        return patcher.start()

    def test_updater_raises_if_resources_are_not_set_properly(self):
        with pytest.raises(ValueError) as excinfo:
            LocalUpdater(
                source={'bucket': 'source'},
                destination={},
                signer=self.signer_instance,
                storage=self.storage,
                permission=self.permission)
        assert str(excinfo.value) == ("Resources should contain both "
                                      "bucket and collection")

    def test_get_source_records_asks_storage_for_records(self):
        records = []
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)

        self.updater.get_source_records(None)
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_get_source_records_asks_storage_for_last_modified_records(self):
        records = []
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)

        self.updater.get_source_records(1234)
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            filters=[Filter('last_modified', 1234, COMPARISON.GT)],
            sorting=[Sort('last_modified', 1)])

    def test_get_destination_records(self):
        # We want to test get_destination_records with some records.
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)
        self.updater.get_destination_records()
        self.storage.collection_timestamp.assert_called_with(
            collection_id='record',
            parent_id='/buckets/destbucket/collections/destcollection')
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/destbucket/collections/destcollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_push_records_to_destination(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        assert self.storage.update.call_count == 3

    def test_push_records_to_destination_raises_if_storage_is_misconfigured(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        self.patch(self.updater, 'get_source_records',
                   return_value=([], 1234))
        with pytest.raises(ValueError):
            self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_removes_deleted_records(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{'id': idx, 'deleted': True, 'last_modified': 42}
                        for idx in range(3, 5)])
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=1324)
        assert self.storage.update.call_count == 2
        assert self.storage.delete.call_count == 2

    def test_push_records_skip_already_deleted_records(self):
        # In case the record doesn't exists in the destination
        # a RecordNotFoundError is raised.
        self.storage.delete.side_effect = RecordNotFoundError()
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{'id': idx, 'deleted': True, 'last_modified': 42}
                       for idx in range(3, 5)])
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        # Calling the updater should not raise the RecordNotFoundError.
        self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_to_destination_with_no_destination_changes(self):
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], None))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater, 'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=None)
        assert self.storage.update.call_count == 3

    def test_set_destination_signature_modifies_the_source_collection(self):
        self.storage.get.return_value = {'id': 1234, 'last_modified': 1234}
        self.updater.set_destination_signature(mock.sentinel.signature,
                                               DummyRequest())

        self.storage.update.assert_called_with(
            collection_id='collection',
            object_id='destcollection',
            parent_id='/buckets/destbucket',
            record={
                'id': 1234,
                'signature': mock.sentinel.signature
            })

    def test_update_source_status_modifies_the_source_collection(self):
        self.storage.get.return_value = {'id': 1234, 'last_modified': 1234,
                                         'status': 'to-sign'}
        self.updater.update_source_status(STATUS.SIGNED, DummyRequest())

        self.storage.update.assert_called_with(
            collection_id='collection',
            object_id='sourcecollection',
            parent_id='/buckets/sourcebucket',
            record={
                'id': 1234,
                'last_reviewer': 'basicauth:bob',
                'status': "signed"
            })

    def test_create_destination_updates_collection_permissions(self):
        collection_id = '/buckets/destbucket/collections/destcollection'
        self.updater.create_destination(DummyRequest())
        self.permission.replace_object_permissions.assert_called_with(
            collection_id,
            {"read": ("system.Everyone",)})

    def test_create_destination_creates_bucket(self):
        self.updater.create_destination(DummyRequest())
        self.storage.create.assert_any_call(
            collection_id='bucket',
            parent_id='',
            record={"id": 'destbucket'})

    def test_create_destination_creates_collection(self):
        bucket_id = '/buckets/destbucket'
        self.updater.create_destination(DummyRequest())
        self.storage.create.assert_any_call(
            collection_id='collection',
            parent_id=bucket_id,
            record={"id": 'destcollection'})

    def test_ensure_resource_exists_handles_uniticy_errors(self):
        self.storage.create.side_effect = UnicityError('id', 'record')
        self.updater._ensure_resource_exists('bucket', '', 'abcd',
                                             DummyRequest())

    def test_sign_and_update_destination(self):
        records = [{'id': idx, 'foo': 'bar %s' % idx, 'last_modified': idx}
                   for idx in range(1, 3)]
        self.storage.get_all.return_value = (records, 2)

        self.patch(self.storage, 'update_records')
        self.patch(self.updater, 'get_destination_records',
                   return_value=([], '0'))
        self.patch(self.updater, 'push_records_to_destination')
        self.patch(self.updater, 'set_destination_signature')
        self.updater.sign_and_update_destination(DummyRequest())

        assert self.updater.get_destination_records.call_count == 1
        assert self.updater.push_records_to_destination.call_count == 1
        assert self.updater.set_destination_signature.call_count == 1
Exemple #12
0
class LocalUpdaterTest(unittest.TestCase):
    def setUp(self):
        self.storage = mock.MagicMock()
        self.permission = mock.MagicMock()
        self.signer_instance = mock.MagicMock()
        self.updater = LocalUpdater(source={
            'bucket': 'sourcebucket',
            'collection': 'sourcecollection'
        },
                                    destination={
                                        'bucket': 'destbucket',
                                        'collection': 'destcollection'
                                    },
                                    signer=self.signer_instance,
                                    storage=self.storage,
                                    permission=self.permission)

        # Resource events are bypassed completely in this test suite.
        patcher = mock.patch('kinto_signer.updater.build_request')
        self.addCleanup(patcher.stop)
        patcher.start()

    def patch(self, obj, *args, **kwargs):
        patcher = mock.patch.object(obj, *args, **kwargs)
        self.addCleanup(patcher.stop)
        return patcher.start()

    def test_updater_raises_if_resources_are_not_set_properly(self):
        with pytest.raises(ValueError) as excinfo:
            LocalUpdater(source={'bucket': 'source'},
                         destination={},
                         signer=self.signer_instance,
                         storage=self.storage,
                         permission=self.permission)
        assert str(excinfo.value) == ("Resources should contain both "
                                      "bucket and collection")

    def test_get_source_records_asks_storage_for_records(self):
        records = []
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)

        self.updater.get_source_records(None)
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_get_source_records_asks_storage_for_last_modified_records(self):
        records = []
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)

        self.updater.get_source_records(1234)
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            filters=[Filter('last_modified', 1234, COMPARISON.GT)],
            sorting=[Sort('last_modified', 1)])

    def test_get_destination_records(self):
        # We want to test get_destination_records with some records.
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)
        self.updater.get_destination_records()
        self.storage.collection_timestamp.assert_called_with(
            collection_id='record',
            parent_id='/buckets/destbucket/collections/destcollection')
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/destbucket/collections/destcollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_push_records_to_destination(self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        assert self.storage.update.call_count == 3

    def test_push_records_to_destination_raises_if_storage_is_misconfigured(
            self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        self.patch(self.updater, 'get_source_records', return_value=([], 1234))
        with pytest.raises(ValueError):
            self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_removes_deleted_records(self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{
            'id': idx,
            'deleted': True,
            'last_modified': 42
        } for idx in range(3, 5)])
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=1324)
        assert self.storage.update.call_count == 2
        assert self.storage.delete.call_count == 2

    def test_push_records_skip_already_deleted_records(self):
        # In case the record doesn't exists in the destination
        # a RecordNotFoundError is raised.
        self.storage.delete.side_effect = RecordNotFoundError()
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{
            'id': idx,
            'deleted': True,
            'last_modified': 42
        } for idx in range(3, 5)])
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        # Calling the updater should not raise the RecordNotFoundError.
        self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_to_destination_with_no_destination_changes(self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], None))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=None)
        assert self.storage.update.call_count == 3

    def test_set_destination_signature_modifies_the_destination_collection(
            self):
        self.storage.get.return_value = {'id': 1234, 'last_modified': 1234}
        self.updater.set_destination_signature(mock.sentinel.signature, {},
                                               DummyRequest())

        self.storage.update.assert_called_with(collection_id='collection',
                                               object_id='destcollection',
                                               parent_id='/buckets/destbucket',
                                               record={
                                                   'id':
                                                   1234,
                                                   'signature':
                                                   mock.sentinel.signature
                                               })

    def test_set_destination_signature_copies_kinto_admin_ui_fields(self):
        self.storage.get.return_value = {
            'id': 1234,
            'sort': '-age',
            'last_modified': 1234
        }
        self.updater.set_destination_signature(mock.sentinel.signature, {
            'displayFields': ['name'],
            'sort': 'size'
        }, DummyRequest())

        self.storage.update.assert_called_with(collection_id='collection',
                                               object_id='destcollection',
                                               parent_id='/buckets/destbucket',
                                               record={
                                                   'id': 1234,
                                                   'signature':
                                                   mock.sentinel.signature,
                                                   'sort': '-age',
                                                   'displayFields': ['name']
                                               })

    def test_update_source_status_modifies_the_source_collection(self):
        self.storage.get.return_value = {
            'id': 1234,
            'last_modified': 1234,
            'status': 'to-sign'
        }
        self.updater.update_source_status(STATUS.SIGNED, DummyRequest())

        self.storage.update.assert_called_with(
            collection_id='collection',
            object_id='sourcecollection',
            parent_id='/buckets/sourcebucket',
            record={
                'id': 1234,
                'last_reviewer': 'basicauth:bob',
                'status': "signed"
            })

    def test_create_destination_updates_collection_permissions(self):
        collection_id = '/buckets/destbucket/collections/destcollection'
        self.updater.create_destination(DummyRequest())
        self.permission.replace_object_permissions.assert_called_with(
            collection_id, {"read": ("system.Everyone", )})

    def test_create_destination_creates_bucket(self):
        self.updater.create_destination(DummyRequest())
        self.storage.create.assert_any_call(collection_id='bucket',
                                            parent_id='',
                                            record={"id": 'destbucket'})

    def test_create_destination_creates_collection(self):
        bucket_id = '/buckets/destbucket'
        self.updater.create_destination(DummyRequest())
        self.storage.create.assert_any_call(collection_id='collection',
                                            parent_id=bucket_id,
                                            record={"id": 'destcollection'})

    def test_ensure_resource_exists_handles_uniticy_errors(self):
        self.storage.create.side_effect = UnicityError('id', 'record')
        self.updater._ensure_resource_exists('bucket', '', 'abcd',
                                             DummyRequest())

    def test_sign_and_update_destination(self):
        records = [{
            'id': idx,
            'foo': 'bar %s' % idx,
            'last_modified': idx
        } for idx in range(1, 3)]
        self.storage.get_all.return_value = (records, 2)

        self.patch(self.storage, 'update_records')
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], '0'))
        self.patch(self.updater, 'push_records_to_destination')
        self.patch(self.updater, 'set_destination_signature')
        self.updater.sign_and_update_destination(DummyRequest(),
                                                 {'id': 'source'})

        assert self.updater.get_destination_records.call_count == 1
        assert self.updater.push_records_to_destination.call_count == 1
        assert self.updater.set_destination_signature.call_count == 1
Exemple #13
0
def sign_collection_data(event, resources):
    """
    Listen to resource change events, to check if a new signature is
    requested.

    When a source collection specified in settings is modified, and its
    new metadata ``status`` is set to ``"to-sign"``, then sign the data
    and update the destination.
    """
    payload = event.payload

    current_user_id = event.request.prefixed_userid
    if current_user_id == _PLUGIN_USERID:
        # Ignore changes made by plugin.
        return

    # Prevent recursivity, since the following operations will alter the current collection.
    impacted_records = list(event.impacted_records)

    for impacted in impacted_records:
        new_collection = impacted['new']
        old_collection = impacted.get('old', {})

        uri = instance_uri(event.request,
                           "collection",
                           bucket_id=payload['bucket_id'],
                           id=new_collection['id'])
        resource = resources.get(uri)

        # Only sign the configured resources.
        if resource is None:
            continue

        registry = event.request.registry
        updater = LocalUpdater(signer=registry.signers[uri],
                               storage=registry.storage,
                               permission=registry.permission,
                               source=resource['source'],
                               destination=resource['destination'])

        review_event_cls = None
        try:
            new_status = new_collection.get("status")
            old_status = old_collection.get("status")

            # Autorize kinto-attachment metadata write access. #190
            event.request._attachment_auto_save = True

            if new_status == STATUS.TO_SIGN:
                # Run signature process (will set `last_reviewer` field).
                updater.sign_and_update_destination(event.request,
                                                    source=new_collection)
                if old_status != STATUS.SIGNED:
                    review_event_cls = signer_events.ReviewApproved

            elif new_status == STATUS.TO_REVIEW:
                if 'preview' in resource:
                    # If preview collection: update and sign preview collection
                    updater.destination = resource['preview']
                    updater.sign_and_update_destination(
                        event.request,
                        source=new_collection,
                        next_source_status=STATUS.TO_REVIEW)
                else:
                    # If no preview collection: just track `last_editor`
                    with updater.send_events(event.request):
                        updater.update_source_editor(event.request)
                review_event_cls = signer_events.ReviewRequested

            elif old_status == STATUS.TO_REVIEW and new_status == STATUS.WORK_IN_PROGRESS:
                review_event_cls = signer_events.ReviewRejected

        except Exception:
            logger.exception("Could not sign '{0}'".format(uri))
            event.request.response.status = 503

        # Notify request of review.
        if review_event_cls:
            payload = payload.copy()
            payload["uri"] = uri
            payload["collection_id"] = new_collection['id']
            review_event = review_event_cls(request=event.request,
                                            payload=payload,
                                            impacted_records=[impacted],
                                            resource=resource,
                                            original_event=event)
            event.request.bound_data.setdefault('kinto_signer.events',
                                                []).append(review_event)
Exemple #14
0
class LocalUpdaterTest(unittest.TestCase):
    def setUp(self):
        self.storage = mock.MagicMock()
        self.permission = mock.MagicMock()
        self.signer_instance = mock.MagicMock()
        self.updater = LocalUpdater(source={
            'bucket': 'sourcebucket',
            'collection': 'sourcecollection'
        },
                                    destination={
                                        'bucket': 'destbucket',
                                        'collection': 'destcollection'
                                    },
                                    signer=self.signer_instance,
                                    storage=self.storage,
                                    permission=self.permission)

        # Resource events are bypassed completely in this test suite.
        patcher = mock.patch('kinto_signer.utils.build_request')
        self.addCleanup(patcher.stop)
        patcher.start()

    def patch(self, obj, *args, **kwargs):
        patcher = mock.patch.object(obj, *args, **kwargs)
        self.addCleanup(patcher.stop)
        return patcher.start()

    def test_updater_raises_if_resources_are_not_set_properly(self):
        with pytest.raises(ValueError) as excinfo:
            LocalUpdater(source={'bucket': 'source'},
                         destination={},
                         signer=self.signer_instance,
                         storage=self.storage,
                         permission=self.permission)
        assert str(excinfo.value) == ("Resources should contain both "
                                      "bucket and collection")

    def test_get_source_records_asks_storage_for_records(self):
        records = []
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)

        self.updater.get_source_records(None)
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_get_source_records_asks_storage_for_last_modified_records(self):
        records = []
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)

        self.updater.get_source_records(1234)
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/sourcebucket/collections/sourcecollection',
            include_deleted=True,
            filters=[Filter('last_modified', 1234, COMPARISON.GT)],
            sorting=[Sort('last_modified', 1)])

    def test_get_destination_records(self):
        # We want to test get_destination_records with some records.
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        count = mock.sentinel.count
        self.storage.get_all.return_value = (records, count)
        self.updater.get_destination_records()
        self.storage.collection_timestamp.assert_called_with(
            collection_id='record',
            parent_id='/buckets/destbucket/collections/destcollection')
        self.storage.get_all.assert_called_with(
            collection_id='record',
            parent_id='/buckets/destbucket/collections/destcollection',
            include_deleted=True,
            sorting=[Sort('last_modified', 1)])

    def test_push_records_to_destination(self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        assert self.storage.update.call_count == 3

    def test_push_records_to_destination_raises_if_storage_is_misconfigured(
            self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([{}], 1324))
        self.patch(self.updater,
                   'get_source_records',
                   return_value=([{}], 1234))
        with pytest.raises(ValueError):
            self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_to_destination_does_not_raises_if_collection_is_empty(
            self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        self.patch(self.updater, 'get_source_records', return_value=([], 1234))
        with pytest.raises(ValueError):
            self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_removes_deleted_records(self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{
            'id': idx,
            'deleted': True,
            'last_modified': 42
        } for idx in range(3, 5)])
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=1324)
        assert self.storage.update.call_count == 2
        assert self.storage.delete.call_count == 2

    def test_push_records_skip_already_deleted_records(self):
        # In case the record doesn't exists in the destination
        # a RecordNotFoundError is raised.
        self.storage.delete.side_effect = RecordNotFoundError()
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], 1324))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(0, 2)]
        records.extend([{
            'id': idx,
            'deleted': True,
            'last_modified': 42
        } for idx in range(3, 5)])
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        # Calling the updater should not raise the RecordNotFoundError.
        self.updater.push_records_to_destination(DummyRequest())

    def test_push_records_to_destination_with_no_destination_changes(self):
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], None))
        records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
        self.patch(self.updater,
                   'get_source_records',
                   return_value=(records, 1325))
        self.updater.push_records_to_destination(DummyRequest())
        self.updater.get_source_records.assert_called_with(last_modified=None)
        assert self.storage.update.call_count == 3

    def test_set_destination_signature_modifies_the_destination_collection(
            self):
        self.storage.get.return_value = {'id': 1234, 'last_modified': 1234}
        self.updater.set_destination_signature(mock.sentinel.signature, {},
                                               DummyRequest())

        self.storage.update.assert_called_with(collection_id='collection',
                                               object_id='destcollection',
                                               parent_id='/buckets/destbucket',
                                               record={
                                                   'id':
                                                   1234,
                                                   'signature':
                                                   mock.sentinel.signature
                                               })

    def test_set_destination_signature_copies_kinto_admin_ui_fields(self):
        self.storage.get.return_value = {
            'id': 1234,
            'sort': '-age',
            'last_modified': 1234
        }
        self.updater.set_destination_signature(mock.sentinel.signature, {
            'displayFields': ['name'],
            'sort': 'size'
        }, DummyRequest())

        self.storage.update.assert_called_with(collection_id='collection',
                                               object_id='destcollection',
                                               parent_id='/buckets/destbucket',
                                               record={
                                                   'id': 1234,
                                                   'signature':
                                                   mock.sentinel.signature,
                                                   'sort': '-age',
                                                   'displayFields': ['name']
                                               })

    def test_update_source_status_modifies_the_source_collection(self):
        self.storage.get.return_value = {
            'id': 1234,
            'last_modified': 1234,
            'status': 'to-sign'
        }

        with mock.patch("kinto_signer.updater.datetime") as mocked:
            mocked.datetime.now().isoformat.return_value = "2018-04-09"
            self.updater.update_source_status(STATUS.SIGNED, DummyRequest())

        self.storage.update.assert_called_with(
            collection_id='collection',
            object_id='sourcecollection',
            parent_id='/buckets/sourcebucket',
            record={
                'id': 1234,
                'last_review_by': 'basicauth:bob',
                'last_review_date': '2018-04-09',
                'last_signature_by': 'basicauth:bob',
                'last_signature_date': '2018-04-09',
                'status': "signed"
            })

    def test_create_destination_updates_collection_permissions(self):
        collection_id = '/buckets/destbucket/collections/destcollection'
        request = DummyRequest()
        request.route_path.return_value = collection_id
        self.updater.create_destination(request)
        request.registry.permission.replace_object_permissions.assert_called_with(
            collection_id, {"read": ("system.Everyone", )})

    def test_create_destination_creates_bucket(self):
        request = DummyRequest()
        self.updater.create_destination(request)
        request.registry.storage.create.assert_any_call(
            collection_id='bucket', parent_id='', record={"id": 'destbucket'})

    def test_create_destination_creates_collection(self):
        bucket_id = '/buckets/destbucket'
        request = DummyRequest()
        self.updater.create_destination(request)
        request.registry.storage.create.assert_any_call(
            collection_id='collection',
            parent_id=bucket_id,
            record={"id": 'destcollection'})

    def test_sign_and_update_destination(self):
        records = [{
            'id': idx,
            'foo': 'bar %s' % idx,
            'last_modified': idx
        } for idx in range(1, 3)]
        self.storage.get_all.return_value = (records, 2)

        self.patch(self.storage, 'update_records')
        self.patch(self.updater,
                   'get_destination_records',
                   return_value=([], '0'))
        self.patch(self.updater, 'push_records_to_destination')
        self.patch(self.updater, 'set_destination_signature')
        self.patch(self.updater, 'invalidate_cloudfront_cache')

        self.updater.sign_and_update_destination(DummyRequest(),
                                                 {'id': 'source'})

        assert self.updater.get_destination_records.call_count == 1
        assert self.updater.push_records_to_destination.call_count == 1
        assert self.updater.set_destination_signature.call_count == 1
        assert self.updater.invalidate_cloudfront_cache.call_count == 1

    def test_refresh_signature_does_not_push_records(self):
        self.storage.get_all.return_value = ([], 2)
        self.patch(self.updater, 'set_destination_signature')
        self.patch(self.updater, 'push_records_to_destination')
        self.patch(self.updater, 'invalidate_cloudfront_cache')

        self.updater.refresh_signature(DummyRequest(), 'signed')

        assert self.updater.invalidate_cloudfront_cache.call_count == 1
        assert self.updater.set_destination_signature.call_count == 1
        assert self.updater.push_records_to_destination.call_count == 0

    def test_refresh_signature_restores_status_on_source(self):
        self.storage.get_all.return_value = ([], 2)
        with mock.patch('kinto_signer.updater.datetime') as mocked:
            mocked.datetime.now.return_value = datetime.datetime(2010, 10, 31)

            self.updater.refresh_signature(DummyRequest(), 'work-in-progress')

        new_attrs = {
            'status': 'work-in-progress',
            'last_signature_by': 'basicauth:bob',
            'last_signature_date': '2010-10-31T00:00:00'
        }
        self.storage.update.assert_any_call(collection_id='collection',
                                            parent_id='/buckets/sourcebucket',
                                            object_id='sourcecollection',
                                            record=new_attrs)

    def test_if_distribution_id_a_cloudfront_invalidation_request_is_triggered(
            self):
        request = mock.MagicMock()
        request.registry.settings = {'signer.distribution_id': 'DWIGHTENIS'}
        with mock.patch('boto3.client') as boto3_client:
            self.updater.invalidate_cloudfront_cache(request, 'tz_1234')
            call_args = boto3_client.return_value.create_invalidation.call_args
            params = call_args[1]
            assert params['DistributionId'] == "DWIGHTENIS"
            assert params['InvalidationBatch']['CallerReference'].startswith(
                'tz_1234-')
            assert params['InvalidationBatch']['Paths'] == {
                'Quantity': 1,
                'Items': ['/v1/*']
            }

    def test_does_not_fail_when_cache_invalidation_does(self):
        request = mock.MagicMock()
        request.registry.settings = {'signer.distribution_id': 'DWIGHTENIS'}
        with mock.patch('boto3.client') as boto3_client:
            boto3_client.return_value.create_invalidation.side_effect = ValueError
            self.updater.invalidate_cloudfront_cache(request, 'tz_1234')

    def test_invalidation_paths_can_be_configured(self):
        request = mock.MagicMock()
        request.registry.settings = {
            'signer.distribution_id':
            'DWIGHTENIS',
            'signer.invalidation_paths':
            '/v1/blocklists* '
            '/v1/buckets/{bucket_id}/collections/{collection_id}*'
        }
        with mock.patch('boto3.client') as boto3_client:
            self.updater.invalidate_cloudfront_cache(request, 'tz_1234')
            call_args = boto3_client.return_value.create_invalidation.call_args
            params = call_args[1]
            assert params['InvalidationBatch']['Paths']['Quantity'] == 2
            assert params['InvalidationBatch']['Paths']['Items'] == [
                '/v1/blocklists*',
                '/v1/buckets/destbucket/collections/destcollection*'
            ]
Exemple #15
0
def sign_collection_data(event, resources, to_review_enabled, **kwargs):
    """
    Listen to resource change events, to check if a new signature is
    requested.

    When a source collection specified in settings is modified, and its
    new metadata ``status`` is set to ``"to-sign"``, then sign the data
    and update the destination.
    """
    payload = event.payload

    is_new_collection = payload['action'] == ACTIONS.CREATE.value

    current_user_id = event.request.prefixed_userid
    if current_user_id == PLUGIN_USERID:
        # Ignore changes made by plugin.
        return

    # Prevent recursivity, since the following operations will alter the current collection.
    impacted_records = list(event.impacted_records)

    for impacted in impacted_records:
        new_collection = impacted['new']
        old_collection = impacted.get('old', {})

        # Only sign the configured resources.
        resource, signer = pick_resource_and_signer(event.request, resources,
                                                    bucket_id=payload['bucket_id'],
                                                    collection_id=new_collection['id'])
        if resource is None:
            continue

        updater = LocalUpdater(signer=signer,
                               storage=event.request.registry.storage,
                               permission=event.request.registry.permission,
                               source=resource['source'],
                               destination=resource['destination'])

        uri = instance_uri(event.request, "collection", bucket_id=payload['bucket_id'],
                           id=new_collection['id'])

        has_review_enabled = ('preview' in resource and
                              resource.get('to_review_enabled', to_review_enabled))

        review_event_cls = None

        new_status = new_collection.get("status")
        old_status = old_collection.get("status")

        # Autorize kinto-attachment metadata write access. #190
        event.request._attachment_auto_save = True

        try:
            if is_new_collection:
                if has_review_enabled:
                    updater.destination = resource['preview']
                    updater.sign_and_update_destination(event.request,
                                                        source_attributes=new_collection,
                                                        next_source_status=None)
                updater.destination = resource['destination']
                updater.sign_and_update_destination(event.request,
                                                    source_attributes=new_collection,
                                                    next_source_status=None)

            if old_status == new_status:
                continue

            if new_status == STATUS.TO_SIGN:
                # Run signature process (will set `last_reviewer` field).
                updater.destination = resource['destination']
                updater.sign_and_update_destination(event.request,
                                                    source_attributes=new_collection,
                                                    previous_source_status=old_status)

                if old_status == STATUS.SIGNED:
                    # When we refresh the signature, it is mainly in order to make sure that
                    # the latest signer certificate was used. When a preview collection
                    # is configured, we also want to refresh its signature.
                    if has_review_enabled:
                        updater.destination = resource['preview']
                        updater.sign_and_update_destination(event.request,
                                                            source_attributes=new_collection,
                                                            previous_source_status=old_status)
                else:
                    review_event_cls = signer_events.ReviewApproved

            elif new_status == STATUS.TO_REVIEW:
                if has_review_enabled:
                    # If preview collection: update and sign preview collection
                    updater.destination = resource['preview']
                    updater.sign_and_update_destination(event.request,
                                                        source_attributes=new_collection,
                                                        next_source_status=STATUS.TO_REVIEW)
                else:
                    # If no preview collection: just track `last_editor`
                    updater.update_source_review_request_by(event.request)
                review_event_cls = signer_events.ReviewRequested

            elif old_status == STATUS.TO_REVIEW and new_status == STATUS.WORK_IN_PROGRESS:
                review_event_cls = signer_events.ReviewRejected

            elif new_status == STATUS.TO_REFRESH:
                updater.refresh_signature(event.request, next_source_status=old_status)
                if has_review_enabled:
                    updater.destination = resource['preview']
                    updater.refresh_signature(event.request, next_source_status=old_status)

        except Exception:
            logger.exception("Could not sign '{0}'".format(uri))
            event.request.response.status = 503

        # Notify request of review.
        if review_event_cls:
            payload = payload.copy()
            payload["uri"] = uri
            payload["collection_id"] = new_collection['id']
            review_event = review_event_cls(request=event.request,
                                            payload=payload,
                                            impacted_records=[impacted],
                                            resource=resource,
                                            original_event=event)
            event.request.bound_data.setdefault('kinto_signer.events', []).append(review_event)