def test_multiple_record_deletion(self): client = Client(server_url=self.server_url, auth=self.auth, bucket='mozilla', collection='payments') client.create_bucket() client.create_collection() client.create_record({'foo': 'bar'}) client.delete_records() assert len(client.get_records()) == 0
def test_multiple_record_deletion(self): client = Client(server_url=self.server_url, auth=self.auth, bucket='mozilla', collection='payments') client.create_bucket() client.create_collection() client.create_record(data={'foo': 'bar'}) client.delete_records() assert len(client.get_records()) == 0
def test_multiple_record_deletion(self): client = Client(server_url=self.server_url, auth=self.auth, bucket="mozilla", collection="payments") client.create_bucket() client.create_collection() client.create_record(data={"foo": "bar"}) client.delete_records() assert len(client.get_records()) == 0
def test_records_deletion_when_no_records_exist(self): client = Client(server_url=self.server_url, auth=self.auth, bucket='mozilla', collection='payments') client.create_bucket() client.create_collection() deleted_records = client.delete_records() assert len(deleted_records) == 0
class RecordLoggingTest(unittest.TestCase): def setUp(self): self.session = mock.MagicMock() self.client = Client(session=self.session) mock_response(self.session) def test_create_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket(id='buck') self.client.create_collection(id='mozilla', bucket='buck') self.client.create_record( id='fake-record', data={'foo': 'bar'}, permissions={'write': ['blah', ]}, bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Create record with id 'fake-record' in collection 'mozilla' in bucket 'buck'") def test_update_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket(id='buck') self.client.create_collection(bucket='buck', id='mozilla') self.client.update_record( id='fake-record', data={'ss': 'aa'}, bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Update record with id 'fake-record' in collection 'mozilla' in bucket 'buck'") def test_get_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket(id='buck') self.client.create_collection(id='mozilla', bucket='buck') self.client.get_record( id='fake-record', bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Get record with id 'fake-record' from collection 'mozilla' in bucket 'buck'") def test_delete_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket(id='buck') self.client.create_collection(id='mozilla', bucket='buck') self.client.delete_record( id='fake-record', bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Delete record with id 'fake-record' from collection 'mozilla' in bucket 'buck'") def test_delete_records_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket(id='buck') self.client.create_collection(id='mozilla', bucket='buck') self.client.delete_records( bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Delete records from collection 'mozilla' in bucket 'buck'")
class RecordLoggingTest(unittest.TestCase): def setUp(self): self.session = mock.MagicMock() self.client = Client(session=self.session) mock_response(self.session) def test_create_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket('buck') self.client.create_collection('mozilla', bucket='buck') self.client.create_record(id='fake-record', data={'foo': 'bar'}, permissions={'write': [ 'blah', ]}, bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Create record with id 'fake-record' in collection 'mozilla' in bucket 'buck'" ) def test_update_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket('buck') self.client.create_collection('mozilla', bucket='buck') self.client.update_record(id='fake-record', data={'ss': 'aa'}, bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Update record with id 'fake-record' in collection 'mozilla' in bucket 'buck'" ) def test_get_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket('buck') self.client.create_collection('mozilla', bucket='buck') self.client.get_record(id='fake-record', bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Get record with id 'fake-record' from collection 'mozilla' in bucket 'buck'" ) def test_delete_record_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket('buck') self.client.create_collection('mozilla', bucket='buck') self.client.delete_record(id='fake-record', bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Delete record with id 'fake-record' from collection 'mozilla' in bucket 'buck'" ) def test_delete_records_logs_info_message(self): with mock.patch('kinto_http.logger') as mocked_logger: self.client.create_bucket('buck') self.client.create_collection('mozilla', bucket='buck') self.client.delete_records(bucket='buck', collection='mozilla') mocked_logger.info.assert_called_with( "Delete records from collection 'mozilla' in bucket 'buck'")
class CollectionTest(unittest.TestCase): def setUp(self): self.session = mock.MagicMock() mock_response(self.session) self.client = Client(session=self.session, bucket='mybucket') def test_collection_names_are_slugified(self): self.client.get_collection('my collection') url = '/buckets/mybucket/collections/my-collection' self.session.request.assert_called_with('get', url) def test_collection_creation_issues_an_http_put(self): self.client.create_collection( 'mycollection', permissions=mock.sentinel.permissions) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'put', url, data=None, permissions=mock.sentinel.permissions, headers=DO_NOT_OVERWRITE) def test_data_can_be_sent_on_creation(self): self.client.create_collection( 'mycollection', 'testbucket', data={'foo': 'bar'}) self.session.request.assert_called_with( 'put', '/buckets/testbucket/collections/mycollection', data={'foo': 'bar'}, permissions=None, headers=DO_NOT_OVERWRITE) def test_collection_update_issues_an_http_put(self): self.client.update_collection( {'foo': 'bar'}, collection='mycollection', permissions=mock.sentinel.permissions) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'put', url, data={'foo': 'bar'}, permissions=mock.sentinel.permissions, headers=None) def test_update_handles_if_match(self): self.client.update_collection( {'foo': 'bar'}, collection='mycollection', if_match=1234) url = '/buckets/mybucket/collections/mycollection' headers = {'If-Match': '"1234"'} self.session.request.assert_called_with( 'put', url, data={'foo': 'bar'}, headers=headers, permissions=None) def test_collection_update_use_an_if_match_header(self): data = {'foo': 'bar', 'last_modified': '1234'} self.client.update_collection( data, collection='mycollection', permissions=mock.sentinel.permissions) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'put', url, data={'foo': 'bar', 'last_modified': '1234'}, permissions=mock.sentinel.permissions, headers={'If-Match': '"1234"'}) def test_patch_collection_issues_an_http_patch(self): self.client.patch_collection( collection='mycollection', data={'key': 'secret'}) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'patch', url, data={'key': 'secret'}, headers=None, permissions=None) def test_patch_collection_handles_if_match(self): self.client.patch_collection( collection='mycollection', data={'key': 'secret'}, if_match=1234) url = '/buckets/mybucket/collections/mycollection' headers = {'If-Match': '"1234"'} self.session.request.assert_called_with( 'patch', url, data={'key': 'secret'}, headers=headers, permissions=None) def test_get_collections_returns_the_list_of_collections(self): mock_response( self.session, data=[ {'id': 'foo', 'last_modified': '12345'}, {'id': 'bar', 'last_modified': '59874'}, ]) collections = self.client.get_collections(bucket='default') assert list(collections) == [ {'id': 'foo', 'last_modified': '12345'}, {'id': 'bar', 'last_modified': '59874'}, ] def test_collection_can_delete_all_its_records(self): self.client.delete_records(bucket='abucket', collection='acollection') url = '/buckets/abucket/collections/acollection/records' self.session.request.assert_called_with('delete', url, headers=None) def test_delete_is_issued_on_list_deletion(self): self.client.delete_collections(bucket='mybucket') url = '/buckets/mybucket/collections' self.session.request.assert_called_with('delete', url, headers=None) def test_collection_can_be_deleted(self): data = {} mock_response(self.session, data=data) deleted = self.client.delete_collection('mycollection') assert deleted == data url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with('delete', url, headers=None) def test_collection_delete_if_match(self): data = {} mock_response(self.session, data=data) deleted = self.client.delete_collection( 'mycollection', if_match=1234) assert deleted == data url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'delete', url, headers={'If-Match': '"1234"'}) def test_collection_delete_if_match_not_included_if_not_safe(self): data = {} mock_response(self.session, data=data) deleted = self.client.delete_collection( 'mycollection', if_match=1324, safe=False) assert deleted == data url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with('delete', url, headers=None) def test_get_or_create_doesnt_raise_in_case_of_conflict(self): data = { 'permissions': mock.sentinel.permissions, 'data': {'foo': 'bar'} } self.session.request.side_effect = [ get_http_error(status=412), (data, None) ] returned_data = self.client.create_collection( bucket="buck", collection="coll", if_not_exists=True) # Should not raise. assert returned_data == data def test_get_or_create_raise_in_other_cases(self): self.session.request.side_effect = get_http_error(status=500) with self.assertRaises(KintoException): self.client.create_collection( bucket="buck", collection="coll", if_not_exists=True) def test_create_collection_raises_a_special_error_on_403(self): self.session.request.side_effect = get_http_error(status=403) with self.assertRaises(KintoException) as e: self.client.create_collection( bucket="buck", collection="coll") expected_msg = ("Unauthorized. Please check that the bucket exists " "and that you have the permission to create or write " "on this collection.") assert e.exception.message == expected_msg
class CollectionTest(unittest.TestCase): def setUp(self): self.session = mock.MagicMock() mock_response(self.session) self.client = Client(session=self.session, bucket='mybucket') def test_collection_names_are_slugified(self): self.client.get_collection(id='my collection') url = '/buckets/mybucket/collections/my-collection' self.session.request.assert_called_with('get', url) def test_collection_creation_issues_an_http_put(self): self.client.create_collection(id='mycollection', permissions=mock.sentinel.permissions) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'put', url, data=None, permissions=mock.sentinel.permissions, headers=DO_NOT_OVERWRITE) def test_data_can_be_sent_on_creation(self): self.client.create_collection(id='mycollection', bucket='testbucket', data={'foo': 'bar'}) self.session.request.assert_called_with( 'put', '/buckets/testbucket/collections/mycollection', data={'foo': 'bar'}, permissions=None, headers=DO_NOT_OVERWRITE) def test_collection_update_issues_an_http_put(self): self.client.update_collection(id='mycollection', data={'foo': 'bar'}, permissions=mock.sentinel.permissions) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'put', url, data={'foo': 'bar'}, permissions=mock.sentinel.permissions, headers=None) def test_update_handles_if_match(self): self.client.update_collection(id='mycollection', data={'foo': 'bar'}, if_match=1234) url = '/buckets/mybucket/collections/mycollection' headers = {'If-Match': '"1234"'} self.session.request.assert_called_with( 'put', url, data={'foo': 'bar'}, headers=headers, permissions=None) def test_collection_update_use_an_if_match_header(self): data = {'foo': 'bar', 'last_modified': '1234'} self.client.update_collection(id='mycollection', data=data, permissions=mock.sentinel.permissions) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'put', url, data={'foo': 'bar', 'last_modified': '1234'}, permissions=mock.sentinel.permissions, headers={'If-Match': '"1234"'}) def test_patch_collection_issues_an_http_patch(self): self.client.patch_collection(id='mycollection', data={'key': 'secret'}) url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'patch', url, payload={'data': {'key': 'secret'}}, headers={'Content-Type': 'application/json'}, ) def test_patch_collection_handles_if_match(self): self.client.patch_collection(id='mycollection', data={'key': 'secret'}, if_match=1234) url = '/buckets/mybucket/collections/mycollection' headers = {'If-Match': '"1234"', 'Content-Type': 'application/json'} self.session.request.assert_called_with( 'patch', url, payload={'data': {'key': 'secret'}}, headers=headers, ) def test_patch_requires_patch_to_be_patch_type(self): with pytest.raises(TypeError): self.client.patch_collection(id='testcoll', bucket='testbucket', changes=5) def test_get_collections_returns_the_list_of_collections(self): mock_response( self.session, data=[ {'id': 'foo', 'last_modified': '12345'}, {'id': 'bar', 'last_modified': '59874'}, ]) collections = self.client.get_collections(bucket='default') assert list(collections) == [ {'id': 'foo', 'last_modified': '12345'}, {'id': 'bar', 'last_modified': '59874'}, ] def test_collection_can_delete_all_its_records(self): self.client.delete_records(bucket='abucket', collection='acollection') url = '/buckets/abucket/collections/acollection/records' self.session.request.assert_called_with('delete', url, headers=None) def test_delete_is_issued_on_list_deletion(self): self.client.delete_collections(bucket='mybucket') url = '/buckets/mybucket/collections' self.session.request.assert_called_with('delete', url, headers=None) def test_collection_can_be_deleted(self): data = {} mock_response(self.session, data=data) deleted = self.client.delete_collection(id='mycollection') assert deleted == data url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with('delete', url, headers=None) def test_collection_delete_if_match(self): data = {} mock_response(self.session, data=data) deleted = self.client.delete_collection(id='mycollection', if_match=1234) assert deleted == data url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with( 'delete', url, headers={'If-Match': '"1234"'}) def test_collection_delete_if_match_not_included_if_not_safe(self): data = {} mock_response(self.session, data=data) deleted = self.client.delete_collection(id='mycollection', if_match=1324, safe=False) assert deleted == data url = '/buckets/mybucket/collections/mycollection' self.session.request.assert_called_with('delete', url, headers=None) def test_get_or_create_doesnt_raise_in_case_of_conflict(self): data = { 'permissions': mock.sentinel.permissions, 'data': {'foo': 'bar'} } self.session.request.side_effect = [ get_http_error(status=412), (data, None) ] returned_data = self.client.create_collection(bucket="buck", id="coll", if_not_exists=True) # Should not raise. assert returned_data == data def test_get_or_create_raise_in_other_cases(self): self.session.request.side_effect = get_http_error(status=500) with self.assertRaises(KintoException): self.client.create_collection(bucket="buck", id="coll", if_not_exists=True) def test_create_collection_raises_a_special_error_on_403(self): self.session.request.side_effect = get_http_error(status=403) with self.assertRaises(KintoException) as e: self.client.create_collection(bucket="buck", id="coll") expected_msg = ("Unauthorized. Please check that the bucket exists " "and that you have the permission to create or write " "on this collection.") assert e.exception.message == expected_msg def test_create_collection_can_deduce_id_from_data(self): self.client.create_collection(data={'id': 'coll'}, bucket='buck') self.session.request.assert_called_with( 'put', '/buckets/buck/collections/coll', data={'id': 'coll'}, permissions=None, headers=DO_NOT_OVERWRITE) def test_update_collection_can_deduce_id_from_data(self): self.client.update_collection(data={'id': 'coll'}, bucket='buck') self.session.request.assert_called_with( 'put', '/buckets/buck/collections/coll', data={'id': 'coll'}, permissions=None, headers=None)
class RecordLoggingTest(unittest.TestCase): def setUp(self): self.session = mock.MagicMock() self.client = Client(session=self.session) mock_response(self.session) def test_create_record_logs_info_message(self): with mock.patch("kinto_http.logger") as mocked_logger: self.client.create_bucket(id="buck") self.client.create_collection(id="mozilla", bucket="buck") self.client.create_record( id="fake-record", data={"foo": "bar"}, permissions={"write": ["blah"]}, bucket="buck", collection="mozilla", ) mocked_logger.info.assert_called_with( "Create record with id 'fake-record' in collection 'mozilla' in bucket 'buck'" ) def test_update_record_logs_info_message(self): with mock.patch("kinto_http.logger") as mocked_logger: self.client.create_bucket(id="buck") self.client.create_collection(bucket="buck", id="mozilla") self.client.update_record(id="fake-record", data={"ss": "aa"}, bucket="buck", collection="mozilla") mocked_logger.info.assert_called_with( "Update record with id 'fake-record' in collection 'mozilla' in bucket 'buck'" ) def test_patch_record_logs_info_message(self): with mock.patch("kinto_http.logger") as mocked_logger: self.client.create_bucket(id="buck") self.client.create_collection(bucket="buck", id="mozilla") self.client.patch_record(id="fake-record", data={"ss": "aa"}, bucket="buck", collection="mozilla") mocked_logger.info.assert_called_with( "Patch record with id 'fake-record' in collection 'mozilla' in bucket 'buck'" ) def test_get_record_logs_info_message(self): with mock.patch("kinto_http.logger") as mocked_logger: self.client.create_bucket(id="buck") self.client.create_collection(id="mozilla", bucket="buck") self.client.get_record(id="fake-record", bucket="buck", collection="mozilla") mocked_logger.info.assert_called_with( "Get record with id 'fake-record' from collection 'mozilla' in bucket 'buck'" ) def test_delete_record_logs_info_message(self): with mock.patch("kinto_http.logger") as mocked_logger: self.client.create_bucket(id="buck") self.client.create_collection(id="mozilla", bucket="buck") self.client.delete_record(id="fake-record", bucket="buck", collection="mozilla") mocked_logger.info.assert_called_with( "Delete record with id 'fake-record' from collection 'mozilla' in bucket 'buck'" ) def test_delete_records_logs_info_message(self): with mock.patch("kinto_http.logger") as mocked_logger: self.client.create_bucket(id="buck") self.client.create_collection(id="mozilla", bucket="buck") self.client.delete_records(bucket="buck", collection="mozilla") mocked_logger.info.assert_called_with( "Delete records from collection 'mozilla' in bucket 'buck'")
def main(): args = _get_args() client = Client(server_url=args.server, auth=tuple(args.auth.split(':')), bucket=args.source_bucket, collection=args.source_col) if args.editor_auth is None: args.editor_auth = args.auth if args.reviewer_auth is None: args.reviewer_auth = args.auth editor_client = Client(server_url=args.server, auth=tuple(args.editor_auth.split(':')), bucket=args.source_bucket, collection=args.source_col) reviewer_client = Client(server_url=args.server, auth=tuple(args.reviewer_auth.split(':')), bucket=args.source_bucket, collection=args.source_col) # 0. initialize source bucket/collection (if necessary) server_info = client.server_info() editor_id = editor_client.server_info()['user']['id'] reviewer_id = reviewer_client.server_info()['user']['id'] print('Server: {0}'.format(args.server)) print('Author: {user[id]}'.format(**server_info)) print('Editor: {0}'.format(editor_id)) print('Reviewer: {0}'.format(reviewer_id)) # 0. check that this collection is well configured. signer_capabilities = server_info['capabilities']['signer'] to_review_enabled = signer_capabilities.get('to_review_enabled', False) group_check_enabled = signer_capabilities.get('group_check_enabled', False) resources = [ r for r in signer_capabilities['resources'] if (args.source_bucket, args.source_col) == (r['source']['bucket'], r['source']['collection']) ] assert len(resources) > 0, 'Specified source not configured to be signed' resource = resources[0] if to_review_enabled and 'preview' in resource: print( 'Signoff: {source[bucket]}/{source[collection]} => {preview[bucket]}/{preview[collection]} => {destination[bucket]}/{destination[collection]}' .format(**resource)) else: print( 'Signoff: {source[bucket]}/{source[collection]} => {destination[bucket]}/{destination[collection]}' .format(**resource)) print('Group check: {0}'.format(group_check_enabled)) print('Review workflow: {0}'.format(to_review_enabled)) print('_' * 80) bucket = client.create_bucket(if_not_exists=True) client.patch_bucket(permissions={ 'write': [editor_id, reviewer_id] + bucket['permissions']['write'] }, if_match=bucket['data']['last_modified'], safe=True) client.create_collection(if_not_exists=True) if args.reset: client.delete_records() existing = 0 else: existing_records = client.get_records() existing = len(existing_records) if group_check_enabled: editors_group = signer_capabilities['editors_group'] client.create_group(editors_group, data={'members': [editor_id]}, if_not_exists=True) reviewers_group = signer_capabilities['reviewers_group'] client.create_group(reviewers_group, data={'members': [reviewer_id]}, if_not_exists=True) dest_client = Client(server_url=args.server, bucket=resource['destination']['bucket'], collection=resource['destination']['collection']) preview_client = None if to_review_enabled and 'preview' in resource: preview_bucket = resource['preview']['bucket'] preview_collection = resource['preview']['collection'] preview_client = Client(server_url=args.server, bucket=preview_bucket, collection=preview_collection) # 1. upload data print('Author uploads 20 random records') records = upload_records(client, 20) # 2. ask for a signature # 2.1 ask for review (noop on old versions) print('Editor asks for review') data = {"status": "to-review"} editor_client.patch_collection(data=data) # 2.2 check the preview collection (if enabled) if preview_client: print('Check preview collection') preview_records = preview_client.get_records() expected = existing + 20 assert len(preview_records) == expected, '%s != %s records' % ( len(preview_records), expected) metadata = preview_client.get_collection()['data'] preview_signature = metadata.get('signature') assert preview_signature, 'Preview collection not signed' preview_timestamp = collection_timestamp(preview_client) # 2.3 approve the review print('Reviewer approves and triggers signature') data = {"status": "to-sign"} reviewer_client.patch_collection(data=data) # 3. upload more data print('Author creates 20 others records') upload_records(client, 20) print('Editor updates 5 random records') for toupdate in random.sample(records, 5): editor_client.patch_record(dict(newkey=_rand(10), **toupdate)) print('Author deletes 5 random records') for todelete in random.sample(records, 5): client.delete_record(todelete['id']) expected = existing + 20 + 20 - 5 # 4. ask again for a signature # 2.1 ask for review (noop on old versions) print('Editor asks for review') data = {"status": "to-review"} editor_client.patch_collection(data=data) # 2.2 check the preview collection (if enabled) if preview_client: print('Check preview collection') preview_records = preview_client.get_records() assert len(preview_records) == expected, '%s != %s records' % ( len(preview_records), expected) # Diff size is 20 + 5 if updated records are also all deleted, # or 30 if deletions and updates apply to different records. diff_since_last = preview_client.get_records(_since=preview_timestamp) assert 25 <= len( diff_since_last ) <= 30, 'Changes since last signature are not consistent' metadata = preview_client.get_collection()['data'] assert preview_signature != metadata[ 'signature'], 'Preview collection not updated' # 2.3 approve the review print('Reviewer approves and triggers signature') data = {"status": "to-sign"} reviewer_client.patch_collection(data=data) # 5. wait for the result # 6. obtain the destination records and serialize canonically. records = list(dest_client.get_records()) assert len(records) == expected, '%s != %s records' % (len(records), expected) timestamp = collection_timestamp(dest_client) serialized = canonical_json(records, timestamp) print('Hash is %r' % compute_hash(serialized)) # 7. get back the signed hash dest_col = dest_client.get_collection() signature = dest_col['data']['signature'] with open('pub', 'w') as f: f.write(signature['public_key']) # 8. verify the signature matches the hash signer = ECDSASigner(public_key='pub') try: signer.verify(serialized, signature) print('Signature OK') except Exception: print('Signature KO') raise
def main(): args = _get_args() client = Client(server_url=args.server, auth=tuple(args.auth.split(':')), bucket=args.source_bucket, collection=args.source_col) if args.editor_auth is None: args.editor_auth = args.auth if args.reviewer_auth is None: args.reviewer_auth = args.auth editor_client = Client(server_url=args.server, auth=tuple(args.editor_auth.split(':')), bucket=args.source_bucket, collection=args.source_col) reviewer_client = Client(server_url=args.server, auth=tuple(args.reviewer_auth.split(':')), bucket=args.source_bucket, collection=args.source_col) # 0. initialize source bucket/collection (if necessary) server_info = client.server_info() editor_id = editor_client.server_info()['user']['id'] reviewer_id = reviewer_client.server_info()['user']['id'] print('Server: {0}'.format(args.server)) print('Author: {user[id]}'.format(**server_info)) print('Editor: {0}'.format(editor_id)) print('Reviewer: {0}'.format(reviewer_id)) # 0. check that this collection is well configured. signer_capabilities = server_info['capabilities']['signer'] resources = [r for r in signer_capabilities['resources'] if (args.source_bucket, args.source_col) == (r['source']['bucket'], r['source']['collection']) or (args.source_bucket, None) == (r['source']['bucket'], r['source']['collection'])] assert len(resources) > 0, 'Specified source not configured to be signed' resource = resources[0] if 'preview' in resource: print('Signoff: {source[bucket]}/{source[collection]} => {preview[bucket]}/{preview[collection]} => {destination[bucket]}/{destination[collection]}'.format(**resource)) else: print('Signoff: {source[bucket]}/{source[collection]} => {destination[bucket]}/{destination[collection]}'.format(**resource)) print('_' * 80) bucket = client.create_bucket(if_not_exists=True) client.create_collection(permissions={'write': [editor_id, reviewer_id] + bucket['permissions']['write']}, if_not_exists=True) editors_group = resource.get('editors_group') or signer_capabilities['editors_group'] editors_group = editors_group.format(collection_id=args.source_col) client.patch_group(id=editors_group, data={'members': [editor_id]}) reviewers_group = resource.get('reviewers_group') or signer_capabilities['reviewers_group'] reviewers_group = reviewers_group.format(collection_id=args.source_col) client.patch_group(id=reviewers_group, data={'members': [reviewer_id]}) if args.reset: client.delete_records() existing = 0 else: existing_records = client.get_records() existing = len(existing_records) dest_col = resource['destination'].get('collection') or args.source_col dest_client = Client(server_url=args.server, bucket=resource['destination']['bucket'], collection=dest_col) preview_client = None if 'preview' in resource: preview_bucket = resource['preview']['bucket'] preview_collection = resource['preview'].get('collection') or args.source_col preview_client = Client(server_url=args.server, bucket=preview_bucket, collection=preview_collection) # 1. upload data print('Author uploads 20 random records') records = upload_records(client, 20) # 2. ask for a signature # 2.1 ask for review (noop on old versions) print('Editor asks for review') data = {"status": "to-review"} editor_client.patch_collection(data=data) # 2.2 check the preview collection (if enabled) if preview_client: print('Check preview collection') preview_records = preview_client.get_records() expected = existing + 20 assert len(preview_records) == expected, '%s != %s records' % (len(preview_records), expected) metadata = preview_client.get_collection()['data'] preview_signature = metadata.get('signature') assert preview_signature, 'Preview collection not signed' preview_timestamp = preview_client.get_records_timestamp() # 2.3 approve the review print('Reviewer approves and triggers signature') data = {"status": "to-sign"} reviewer_client.patch_collection(data=data) # 3. upload more data print('Author creates 20 others records') upload_records(client, 20) print('Editor updates 5 random records') for toupdate in random.sample(records, 5): editor_client.patch_record(data=dict(newkey=_rand(10), **toupdate)) print('Author deletes 5 random records') for todelete in random.sample(records, 5): client.delete_record(id=todelete['id']) expected = existing + 20 + 20 - 5 # 4. ask again for a signature # 2.1 ask for review (noop on old versions) print('Editor asks for review') data = {"status": "to-review"} editor_client.patch_collection(data=data) # 2.2 check the preview collection (if enabled) if preview_client: print('Check preview collection') preview_records = preview_client.get_records() assert len(preview_records) == expected, '%s != %s records' % (len(preview_records), expected) # Diff size is 20 + 5 if updated records are also all deleted, # or 30 if deletions and updates apply to different records. diff_since_last = preview_client.get_records(_since=preview_timestamp) assert 25 <= len(diff_since_last) <= 30, 'Changes since last signature are not consistent' metadata = preview_client.get_collection()['data'] assert preview_signature != metadata['signature'], 'Preview collection not updated' # 2.3 approve the review print('Reviewer approves and triggers signature') data = {"status": "to-sign"} reviewer_client.patch_collection(data=data) # 5. wait for the result # 6. obtain the destination records and serialize canonically. records = list(dest_client.get_records()) assert len(records) == expected, '%s != %s records' % (len(records), expected) timestamp = dest_client.get_records_timestamp() serialized = canonical_json(records, timestamp) print('Hash is %r' % compute_hash(serialized)) # 7. get back the signed hash signature = dest_client.get_collection()['data']['signature'] with open('pub', 'w') as f: f.write(signature['public_key']) # 8. verify the signature matches the hash signer = ECDSASigner(public_key='pub') try: signer.verify(serialized, signature) print('Signature OK') except Exception: print('Signature KO') raise