Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Commit

Permalink
Merge cb1d1d8 into 40736c5
Browse files Browse the repository at this point in the history
  • Loading branch information
leplatrem committed Jul 21, 2016
2 parents 40736c5 + cb1d1d8 commit c57e252
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 84 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ This document describes changes between each past release.
- Update the `last_modified` value when updating the collection status and signature (#97)
- Prevents crash with events on ``default`` bucket on Kinto < 3.3

**New features**

- Trigger ``ResourceChanged`` events when the destination collection and records are updated
during signing. This allows plugins like ``kinto-changes`` and ``kinto.plugins.history``
to catch the changes.


0.7.0 (2016-06-28)
------------------
Expand Down
2 changes: 1 addition & 1 deletion kinto_signer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def on_collection_changed(event, resources):
destination=resource['destination'])

try:
updater.sign_and_update_destination()
updater.sign_and_update_destination(event.request)
except Exception:
logger.exception("Could not sign '{0}'".format(key))
event.request.response.status = 503
Expand Down
76 changes: 41 additions & 35 deletions kinto_signer/tests/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ def tearDown(self):
# Delete all the created objects.
self._flush_server(self.server_url)

def setUp(self):
# Give the permission to tigger signatures to anybody
perms = {"write": ["system.Authenticated"]}
self.source.create_bucket()
self.source.create_collection(permissions=perms)

# Create some data on the source collection and send it.
with self.source.batch() as batch:
for n in range(0, 10):
batch.create_record(data={'newdata': n})

self.source_records = self.source.get_records()
assert len(self.source_records) == 10

# Trigger a signature.
self.source.update_collection(data={'status': 'to-sign'})

# Wait so the new last_modified timestamp will be greater than the
# one from the previous records.
time.sleep(0.01)

def _flush_server(self, server_url):
flush_url = urljoin(self.server_url, '/__flush__')
resp = requests.post(flush_url)
Expand All @@ -49,22 +70,6 @@ def test_heartbeat_is_successful(self):
resp.raise_for_status()

def test_destination_creation_and_new_records_signature(self):
self.source.create_bucket()
self.source.create_collection()

# Send new data to the signer.
with self.source.batch() as batch:
for n in range(0, 10):
batch.create_record(data={'newdata': n})

source_records = self.source.get_records()
assert len(source_records) == 10

# Trigger a signature.
self.source.update_collection(
data={'status': 'to-sign'},
method="put")

# Ensure the destination data is signed properly.
data = self.destination.get_collection()
signature = data['data']['signature']
Expand All @@ -81,26 +86,9 @@ def test_destination_creation_and_new_records_signature(self):
assert source_collection['status'] == 'signed'

def test_records_deletion_and_signature(self):
self.source.create_bucket()
self.source.create_collection()

# Create some data on the source collection and send it.
with self.source.batch() as batch:
for n in range(0, 10):
batch.create_record(data={'newdata': n})

source_records = self.source.get_records()
assert len(source_records) == 10

# Trigger a signature.
self.source.update_collection(data={'status': 'to-sign'}, method="put")

# Wait so the new last_modified timestamp will be greater than the
# one from the previous records.
time.sleep(0.01)
# Now delete one record on the source and trigger another signature.
self.source.delete_record(source_records[0]['id'])
self.source.update_collection(data={'status': 'to-sign'}, method="put")
self.source.delete_record(self.source_records[0]['id'])
self.source.update_collection(data={'status': 'to-sign'})

data = self.destination.get_collection()
signature = data['data']['signature']
Expand All @@ -113,6 +101,24 @@ def test_records_deletion_and_signature(self):
# This raises when the signature is invalid.
self.signer.verify(serialized_records, signature)

def test_distinct_users_can_trigger_signatures(self):
collection = self.destination.get_collection()
before = collection['data']['signature']

self.source.create_record(data={"pim": "pam"})
client = Client(
server_url=self.server_url,
auth=("Sam", "Wan-Elss"),
bucket=self.source_bucket,
collection=self.source_collection)
# Trigger a signature as someone else.
client.update_collection(data={'status': 'to-sign'})

collection = self.destination.get_collection()
after = collection['data']['signature']

assert before != after


class AliceFunctionalTest(BaseTestFunctional, unittest2.TestCase):
private_key = os.path.join(__HERE__, 'config/ecdsa.private.pem')
Expand Down
135 changes: 135 additions & 0 deletions kinto_signer/tests/test_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import os

from kinto.tests.support import BaseWebTest, unittest

here = os.path.abspath(os.path.dirname(__file__))


class Listener(object):
def __init__(self):
self.received = []

def __call__(self, event):
self.received.append(event)


listener = Listener()


def load_from_config(config, prefix):
return listener


class ResourceEventsTest(BaseWebTest, unittest.TestCase):
def get_app_settings(self, extra=None):
settings = super(ResourceEventsTest, self).get_app_settings(extra)

settings['storage_backend'] = 'kinto.core.storage.memory'
settings['cache_backend'] = 'kinto.core.cache.memory'
settings['permission_backend'] = 'kinto.core.permission.memory'

settings['includes'] = 'kinto_signer'
settings['signer.ecdsa.private_key'] = os.path.join(
here, 'config', 'ecdsa.private.pem')

self.source_collection = "/buckets/alice/collections/scid"
self.destination_collection = "/buckets/destination/collections/dcid"

settings['signer.resources'] = '%s;%s' % (
self.source_collection,
self.destination_collection)

settings['event_listeners'] = 'ks'
settings['event_listeners.ks.use'] = 'kinto_signer.tests.test_events'
return settings

def setUp(self):
super(ResourceEventsTest, self).setUp()
self.app.put_json("/buckets/alice", headers=self.headers)
self.app.put_json(self.source_collection, headers=self.headers)
self.app.post_json(self.source_collection + "/records",
{"data": {"title": "hello"}},
headers=self.headers)
self.app.post_json(self.source_collection + "/records",
{"data": {"title": "bonjour"}},
headers=self.headers)

def _sign(self):
self.app.patch_json(self.source_collection,
{"data": {"status": "to-sign"}},
headers=self.headers)

resp = self.app.get(self.source_collection, headers=self.headers)
data = resp.json["data"]
self.assertEqual(data["status"], "signed")

resp = self.app.get(self.destination_collection, headers=self.headers)
data = resp.json["data"]
self.assertIn("signature", data)

def test_resource_changed_is_triggered_for_destination_creation(self):
self._sign()
event = [e for e in listener.received
if e.payload["uri"] == "/buckets/destination" and
e.payload["action"] == "create"][0]
self.assertEqual(len(event.impacted_records), 1)

event = [e for e in listener.received
if e.payload["uri"] == self.destination_collection and
e.payload["action"] == "create"][0]
self.assertEqual(len(event.impacted_records), 1)

def test_resource_changed_is_triggered_for_source_collection(self):
before = len(listener.received)

self._sign()
event = [e for e in listener.received[before:]
if e.payload["resource_name"] == "collection" and
e.payload["collection_id"] == "scid" and
e.payload["action"] == "update"][0]
self.assertEqual(len(event.impacted_records), 2)
self.assertEqual(event.impacted_records[0]["new"]["status"], "to-sign")
self.assertEqual(event.impacted_records[1]["new"]["status"], "signed")

def test_resource_changed_is_triggered_for_destination_collection(self):
before = len(listener.received)

self._sign()
event = [e for e in listener.received[before:]
if e.payload["resource_name"] == "collection" and
e.payload.get("collection_id") == "dcid" and
e.payload["action"] == "update"][0]

self.assertEqual(len(event.impacted_records), 1)
self.assertNotEqual(event.impacted_records[0]["old"].get("signature"),
event.impacted_records[0]["new"]["signature"])

def test_resource_changed_is_triggered_for_destination_records(self):
before = len(listener.received)

self._sign()
events = [e for e in listener.received[before:]
if e.payload["resource_name"] == "record" and
e.payload["collection_id"] == "dcid"]

self.assertEqual(len(events), 1)
self.assertEqual(len(events[0].impacted_records), 2)

def test_resource_changed_is_triggered_for_destination_removal(self):
record_uri = self.source_collection + "/records/xyz"
self.app.put_json(record_uri,
{"data": {"title": "servus"}},
headers=self.headers)
self._sign()
self.app.delete(record_uri, headers=self.headers)

before = len(listener.received)
self._sign()

events = [e for e in listener.received[before:]
if e.payload["resource_name"] == "record"]

self.assertEqual(len(events), 1)
self.assertEqual(events[0].payload["action"], "delete")
self.assertEqual(events[0].payload["uri"],
self.destination_collection + "/records/xyz")
31 changes: 20 additions & 11 deletions kinto_signer/tests/test_updater.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import mock
import pytest

from kinto.core.storage import Filter
from kinto.core.storage.exceptions import UnicityError, RecordNotFoundError
from kinto.core.utils import COMPARISON
from kinto.tests.core.support import DummyRequest

from kinto_signer.updater import LocalUpdater
from .support import unittest
Expand All @@ -25,6 +27,11 @@ def setUp(self):
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)
Expand Down Expand Up @@ -82,7 +89,7 @@ def test_push_records_to_destination(self):
records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
self.patch(self.updater, 'get_source_records',
return_value=(records, '42'))
self.updater.push_records_to_destination()
self.updater.push_records_to_destination(DummyRequest())
assert self.storage.update.call_count == 3

def test_push_records_removes_deleted_records(self):
Expand All @@ -93,7 +100,7 @@ def test_push_records_removes_deleted_records(self):
for idx in range(3, 5)])
self.patch(self.updater, 'get_source_records',
return_value=(records, '42'))
self.updater.push_records_to_destination()
self.updater.push_records_to_destination(DummyRequest())
self.updater.get_source_records.assert_called_with(
1324, include_deleted=True)
assert self.storage.update.call_count == 2
Expand All @@ -111,22 +118,23 @@ def test_push_records_skip_already_deleted_records(self):
self.patch(self.updater, 'get_source_records',
return_value=(records, '42'))
# Calling the updater should not raise the RecordNotFoundError.
self.updater.push_records_to_destination()
self.updater.push_records_to_destination(DummyRequest())

def test_push_records_to_destination_with_no_destination_changes(self):
self.patch(self.updater, 'get_destination_last_modified',
return_value=(1324, 0))
records = [{'id': idx, 'foo': 'bar %s' % idx} for idx in range(1, 4)]
self.patch(self.updater, 'get_source_records',
return_value=(records, '42'))
self.updater.push_records_to_destination()
self.updater.push_records_to_destination(DummyRequest())
self.updater.get_source_records.assert_called_with(
None, include_deleted=True)
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)
self.updater.set_destination_signature(mock.sentinel.signature,
DummyRequest())

self.storage.update.assert_called_with(
collection_id='collection',
Expand All @@ -140,7 +148,7 @@ def test_set_destination_signature_modifies_the_source_collection(self):
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("signed")
self.updater.update_source_status("signed", DummyRequest())

self.storage.update.assert_called_with(
collection_id='collection',
Expand All @@ -153,29 +161,30 @@ def test_update_source_status_modifies_the_source_collection(self):

def test_create_destination_updates_collection_permissions(self):
collection_id = '/buckets/destbucket/collections/destcollection'
self.updater.create_destination()
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()
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()
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')
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}
Expand All @@ -186,7 +195,7 @@ def test_sign_and_update_destination(self):
self.patch(self.updater, 'get_source_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()
self.updater.sign_and_update_destination(DummyRequest())

assert self.updater.get_source_records.call_count == 1
assert self.updater.push_records_to_destination.call_count == 1
Expand Down
Loading

0 comments on commit c57e252

Please sign in to comment.