def test_missing_semicolumn_raises_an_exception(self): raw_resources = """ foo bar """ with pytest.raises(ConfigurationError): utils.parse_resources(raw_resources)
def test_non_local_first_argument_raises_an_exception(self): raw_resources = """ foo;bar bar;baz """ with pytest.raises(ConfigurationError): utils.parse_resources(raw_resources)
def test_missing_semicolumn_raises_an_exception(self): raw_resources = """ foo bar """ with pytest.raises(ValueError) as excinfo: utils.parse_resources(raw_resources) msg = "'bucket/coll;bucket/coll'" assert msg in str(excinfo.value)
def test_outnumbered_urls_raises_an_exception(self): raw_resources = ( "/buckets/sbid/scid;" "/buckets/dbid/collections/dcid;" "/buckets/dbid/collections/dcid;" "/buckets/sbid/scid;" ) with pytest.raises(ConfigurationError): utils.parse_resources(raw_resources)
def test_non_local_first_argument_raises_an_exception(self): raw_resources = """ foo;bar bar;baz """ with pytest.raises(ValueError) as excinfo: utils.parse_resources(raw_resources) msg = "Resources should be defined as bucket/collection." assert msg in str(excinfo.value)
def test_resources_should_be_space_separated(self): raw_resources = ( "/buckets/sbid1/collections/scid;/buckets/dbid1/collections/dcid," "/buckets/sbid2/collections/scid;/buckets/dbid2/collections/dcid" ) with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources) raw_resources = "sbid1/scid;dbid1/dcid,sbid2/scid;dbid2/dcid" with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources)
def test_nothing_happens_when_status_is_not_to_sign(self): evt = mock.MagicMock(payload={"action": "update", "bucket_id": "a", "collection_id": "b"}, impacted_objects=[{ "new": {"id": "b", "status": "signed"}}]) sign_collection_data(evt, resources=utils.parse_resources("a/b -> c/d"), to_review_enabled=True) assert not self.updater_mocked.sign_and_update_destination.called
def test_multiple_resources_are_supported(self): raw_resources = """ origin/coll1;dest/coll1 origin/coll2;dest/coll2 """ resources = utils.parse_resources(raw_resources) assert len(resources) == 2
def test_multiple_resources_are_supported(self): raw_resources = """ /buckets/sbid1/collections/scid1;/buckets/dbid1/collections/dcid1 /buckets/sbid2/collections/scid2;/buckets/dbid2/collections/dcid2 """ resources = utils.parse_resources(raw_resources) assert len(resources) == 2
def test_returned_resources_match_the_legacy_format(self): raw_resources = """ sbid/scid;dbid/dcid """ resources = utils.parse_resources(raw_resources) assert resources == { "/buckets/sbid/collections/scid": { "source": {"bucket": "sbid", "collection": "scid"}, "destination": {"bucket": "dbid", "collection": "dcid"}, } }
def includeme(config): settings = config.get_settings() # Load the signer from its dotted location. Fallback to the local ECDSA # signer. default_signer_module = "kinto_signer.signer.local_ecdsa" signer_dotted_location = settings.get( 'signer.signer_backend', default_signer_module) signer_module = config.maybe_dotted(signer_dotted_location) config.registry.signer = signer_module.load_from_settings(settings) # Check source and destination resources are configured. raw_resources = settings.get('signer.resources') if raw_resources is None: raise ValueError("Please specify the kinto_signer.resources value.") available_resources = utils.parse_resources(raw_resources) # Expose the capabilities in the root endpoint. message = "Provide signing capabilities to the server." docs = "https://github.com/mozilla-services/kinto-signer#kinto-signer" resources = sorted(available_resources.keys()) config.add_api_capability("signer", message, docs, resources=resources) # Listen to resource change events, to check if a new signature is # requested. 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() config.add_subscriber( on_resource_changed, ResourceChanged, for_actions=('create', 'update'), for_resources=('collection',) )
def test_a_preview_collection_is_supported(self): raw_resources = ( "/buckets/stage/collections/cid;" "/buckets/preview/collections/cid;" "/buckets/prod/collections/cid;" ) resources = utils.parse_resources(raw_resources) assert resources == { "/buckets/stage/collections/cid": { "source": {"bucket": "stage", "collection": "cid"}, "preview": {"bucket": "preview", "collection": "cid"}, "destination": {"bucket": "prod", "collection": "cid"}, } }
def test_kinto_attachment_property_is_set_to_allow_metadata_updates(self): evt = mock.MagicMock(payload={"action": "update", "bucket_id": "a", "collection_id": "b"}, impacted_objects=[{ "new": {"id": "b", "status": "to-sign"}}]) evt.request.registry.storage = mock.sentinel.storage evt.request.registry.permission = mock.sentinel.permission evt.request.registry.signers = { "/buckets/a/collections/b": mock.sentinel.signer } evt.request.route_path.return_value = "/v1/buckets/a/collections/b" sign_collection_data(evt, resources=utils.parse_resources("a/b -> c/d"), to_review_enabled=True) assert evt.request._attachment_auto_save is True
def test_returned_resources_match_the_expected_format(self): raw_resources = """ sourcebucket/sourcecoll;destinationbucket/destinationcoll """ resources = utils.parse_resources(raw_resources) assert resources == { 'sourcebucket/sourcecoll': { 'source': { 'bucket': 'sourcebucket', 'collection': 'sourcecoll' }, 'destination': { 'bucket': 'destinationbucket', 'collection': 'destinationcoll' } } }
def test_updater_is_called_when_resource_and_status_matches(self): evt = mock.MagicMock(payload={"bucket_id": "a", "collection_id": "b"}, impacted_records=[{ "new": {"id": "b", "status": "to-sign"}}]) evt.request.registry.storage = mock.sentinel.storage evt.request.registry.permission = mock.sentinel.permission evt.request.registry.signers = { "/buckets/a/collections/b": mock.sentinel.signer } evt.request.route_path.return_value = "/v1/buckets/a/collections/b" sign_collection_data(evt, resources=utils.parse_resources("a/b;c/d")) self.updater_mocked.assert_called_with( signer=mock.sentinel.signer, storage=mock.sentinel.storage, permission=mock.sentinel.permission, source={"bucket": "a", "collection": "b"}, destination={"bucket": "c", "collection": "d"}) mocked = self.updater_mocked.return_value assert mocked.sign_and_update_destination.called
def includeme(config): # We import stuff here, so that kinto-signer can be installed with `--no-deps` # and used without having this Pyramid ecosystem installed. import transaction from kinto.core.events import ACTIONS, ResourceChanged from pyramid.exceptions import ConfigurationError from pyramid.events import NewRequest from pyramid.settings import asbool from kinto_signer.signer import heartbeat from kinto_signer import utils from kinto_signer import listeners # Register heartbeat to check signer integration. config.registry.heartbeats['signer'] = heartbeat settings = config.get_settings() # Check source and destination resources are configured. raw_resources = settings.get('signer.resources') if raw_resources is None: error_msg = "Please specify the kinto.signer.resources setting." raise ConfigurationError(error_msg) resources = utils.parse_resources(raw_resources) # Expand the resources with the ones that come from per-bucket resources # and have specific settings. # For example, consider the case where resource is ``/buckets/dev -> /buckets/prod`` # and there is a setting ``signer.dev.recipes.signer_backend = foo`` output_resources = resources.copy() for key, resource in resources.items(): # If collection is not None, there is nothing to expand :) if resource['source']['collection'] is not None: continue bid = resource['source']['bucket'] # Match setting names like signer.stage.specific.autograph.hawk_id matches = [(v, re.search(r'signer\.{0}\.([^\.]+)\.(.+)'.format(bid), k)) for k, v in settings.items()] found = [(v, m.group(1), m.group(2)) for (v, m) in matches if m] # Expand the list of resources with the ones that contain collection # specific settings. for setting_value, cid, setting_name in found: signer_key = "/buckets/{0}/collections/{1}".format(bid, cid) if signer_key not in output_resources: specific = copy.deepcopy(resource) specific["source"]["collection"] = cid specific["destination"]["collection"] = cid if "preview" in specific: specific["preview"]["collection"] = cid output_resources[signer_key] = specific output_resources[signer_key][setting_name] = setting_value resources = output_resources # Determine which are the settings that apply to all buckets/collections. defaults = { "reviewers_group": "reviewers", "editors_group": "editors", "to_review_enabled": False, "group_check_enabled": False, } global_settings = {} for setting in listeners.REVIEW_SETTINGS: value = settings.get("signer.%s" % setting, defaults[setting]) if setting.endswith("_enabled"): value = asbool(value) global_settings[setting] = value # For each resource that is configured, we determine what signer is # configured and what are the review settings. # Note: the `resource` values are mutated in place. config.registry.signers = {} for signer_key, resource in resources.items(): server_wide = 'signer.' bucket_wide = 'signer.{bucket}.'.format(**resource['source']) prefixes = [bucket_wide, server_wide] per_bucket_config = resource['source']['collection'] is None if not per_bucket_config: collection_wide = 'signer.{bucket}.{collection}.'.format(**resource['source']) deprecated = 'signer.{bucket}_{collection}.'.format(**resource['source']) prefixes = [collection_wide, deprecated] + prefixes # Instantiates the signers associated to this resource. dotted_location = utils.get_first_matching_setting('signer_backend', settings, prefixes, default=DEFAULT_SIGNER) signer_module = config.maybe_dotted(dotted_location) backend = signer_module.load_from_settings(settings, prefixes=prefixes) config.registry.signers[signer_key] = backend # Load the setttings associated to each resource. for setting in listeners.REVIEW_SETTINGS: # Per collection/bucket: value = utils.get_first_matching_setting(setting, settings, prefixes, default=global_settings[setting]) if setting.endswith("_enabled"): value = asbool(value) # Resolve placeholder with source info. if setting.endswith("_group"): # If configured per bucket, then we leave the placeholder. # It will be resolved in listeners during group checking and # by Kinto-Admin when matching user groups with info from capabilities. collection_id = resource['source']['collection'] or "{collection_id}" try: value = value.format(bucket_id=resource['source']['bucket'], collection_id=collection_id) except KeyError as e: raise ConfigurationError("Unknown group placeholder %s" % e) # Only expose if relevant. if value != global_settings[setting]: resource[setting] = value else: resource.pop(setting, None) # Expose the capabilities in the root endpoint. exposed_resources = get_exposed_resources(resources, listeners.REVIEW_SETTINGS) message = "Digital signatures for integrity and authenticity of records." docs = "https://github.com/Kinto/kinto-signer#kinto-signer" config.add_api_capability("signer", message, docs, version=__version__, resources=exposed_resources, **global_settings) config.add_subscriber( functools.partial(listeners.set_work_in_progress_status, resources=resources), ResourceChanged, for_resources=('record',)) config.add_subscriber( functools.partial(listeners.check_collection_status, resources=resources, **global_settings), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.check_collection_tracking, resources=resources), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.create_editors_reviewers_groups, resources=resources, editors_group=global_settings["editors_group"], reviewers_group=global_settings["reviewers_group"]), ResourceChanged, for_actions=(ACTIONS.CREATE,), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.cleanup_preview_destination, resources=resources), ResourceChanged, for_actions=(ACTIONS.DELETE,), for_resources=('collection',)) sign_data_listener = functools.partial(listeners.sign_collection_data, resources=resources, **global_settings) # If StatsD is enabled, monitor execution time of listener. if config.registry.statsd: # Due to https://github.com/jsocol/pystatsd/issues/85 for attr in ('__module__', '__name__'): origin = getattr(listeners.sign_collection_data, attr) setattr(sign_data_listener, attr, origin) statsd_client = config.registry.statsd key = 'plugins.signer' sign_data_listener = statsd_client.timer(key)(sign_data_listener) config.add_subscriber( sign_data_listener, ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) def on_new_request(event): """Send the kinto-signer events in the before commit hook. This allows database operations done in subscribers to be automatically committed or rolledback. """ # Since there is one transaction per batch, ignore subrequests. if hasattr(event.request, 'parent'): return current = transaction.get() current.addBeforeCommitHook(listeners.send_signer_events, args=(event,)) config.add_subscriber(on_new_request, NewRequest)
def includeme(config): # Register heartbeat to check signer integration. config.registry.heartbeats['signer'] = heartbeat settings = config.get_settings() # Check source and destination resources are configured. raw_resources = settings.get('signer.resources') if raw_resources is None: error_msg = "Please specify the kinto.signer.resources setting." raise ConfigurationError(error_msg) resources = utils.parse_resources(raw_resources) reviewers_group = settings.get("signer.reviewers_group", "reviewers") editors_group = settings.get("signer.editors_group", "editors") to_review_enabled = asbool(settings.get("signer.to_review_enabled", False)) group_check_enabled = asbool( settings.get("signer.group_check_enabled", False)) config.registry.signers = {} for key, resource in resources.items(): # Load the signers associated to each resource. dotted_location, prefix = _signer_dotted_location(settings, resource) signer_module = config.maybe_dotted(dotted_location) backend = signer_module.load_from_settings(settings, prefix) config.registry.signers[key] = backend # Load the setttings associated to each resource. prefix = "{source[bucket]}_{source[collection]}".format(**resource) for setting in ("reviewers_group", "editors_group", "to_review_enabled", "group_check_enabled"): value = settings.get( "signer.%s.%s" % (prefix, setting), settings.get("signer.%s_%s" % (prefix, setting))) if value is not None: resource[setting] = value # Expose the capabilities in the root endpoint. message = "Digital signatures for integrity and authenticity of records." docs = "https://github.com/Kinto/kinto-signer#kinto-signer" config.add_api_capability("signer", message, docs, version=__version__, resources=resources.values(), to_review_enabled=to_review_enabled, group_check_enabled=group_check_enabled, editors_group=editors_group, reviewers_group=reviewers_group) config.add_subscriber(functools.partial( listeners.set_work_in_progress_status, resources=resources), ResourceChanged, for_resources=('record', )) config.add_subscriber(functools.partial( listeners.check_collection_status, resources=resources, to_review_enabled=to_review_enabled, group_check_enabled=group_check_enabled, editors_group=editors_group, reviewers_group=reviewers_group), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection', )) config.add_subscriber(functools.partial( listeners.check_collection_tracking, resources=resources), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection', )) sign_data_listener = functools.partial(listeners.sign_collection_data, resources=resources) # If StatsD is enabled, monitor execution time of listener. if config.registry.statsd: # Due to https://github.com/jsocol/pystatsd/issues/85 for attr in ('__module__', '__name__'): origin = getattr(listeners.sign_collection_data, attr) setattr(sign_data_listener, attr, origin) statsd_client = config.registry.statsd key = 'plugins.signer' sign_data_listener = statsd_client.timer(key)(sign_data_listener) config.add_subscriber(sign_data_listener, ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection', )) config.add_subscriber(listeners.send_review_events, AfterResourceChanged)
def test_resources_must_be_valid_names(self): raw_resources = "/buckets/sbi+d1/collections/scid;/buckets/dbid1/collections/dci,d" with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources)
def test_cannot_repeat_resources(self): # Repeated source. raw_resources = """ /buckets/stage -> /buckets/preview1 -> /buckets/prod1 /buckets/stage -> /buckets/preview2 -> /buckets/prod2 """ with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources) # Repeated reviews. raw_resources = """ /buckets/stage1 -> /buckets/preview -> /buckets/prod1 /buckets/stage2 -> /buckets/preview -> /buckets/prod2 """ with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources) # Repeated destination. raw_resources = """ /buckets/stage1 -> /buckets/prod /buckets/stage2 -> /buckets/preview -> /buckets/prod """ with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources) # Source in other's preview. raw_resources = """ /buckets/stage -> /buckets/preview -> /buckets/prod /buckets/bid1 -> /buckets/stage -> /buckets/bid2 """ with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources) # Source in other's destination. raw_resources = """ /buckets/b/collections/c -> /buckets/b/collections/c2 -> /buckets/b/collections/c3 /buckets/b/collections/ca -> /buckets/b/collections/cb -> /buckets/b/collections/c """ with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources) # Preview in other's destination. raw_resources = """ /buckets/b/collections/c0 -> /buckets/b/collections/c1 -> /buckets/b/collections/c2 /buckets/b/collections/ca -> /buckets/b/collections/cb -> /buckets/b/collections/c1 """ with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources)
def test_missing_arrow_raises_an_exception(self): raw_resources = """ foo bar """ with pytest.raises(ConfigurationError): utils.parse_resources(raw_resources)
def test_resources_must_be_valid_names(self): raw_resources = ( "/buckets/sbi+d1/collections/scid;/buckets/dbid1/collections/dci,d" ) with self.assertRaises(ConfigurationError): utils.parse_resources(raw_resources)
def test_malformed_url_raises_an_exception(self): raw_resources = """ /buckets/sbid/scid;/buckets/dbid/collections/dcid """ with pytest.raises(ConfigurationError): utils.parse_resources(raw_resources)
def test_nothing_happens_when_resource_is_not_configured(self): evt = mock.MagicMock(payload={"action": "update", "bucket_id": "a", "collection_id": "b"}) sign_collection_data(evt, resources=utils.parse_resources("c/d -> e/f"), to_review_enabled=True) assert not self.updater_mocked.called
def includeme(config): # Register heartbeat to check signer integration. config.registry.heartbeats['signer'] = heartbeat settings = config.get_settings() # Check source and destination resources are configured. raw_resources = settings.get('signer.resources') if raw_resources is None: error_msg = "Please specify the kinto.signer.resources setting." raise ConfigurationError(error_msg) resources = utils.parse_resources(raw_resources) reviewers_group = settings.get("signer.reviewers_group", "reviewers") editors_group = settings.get("signer.editors_group", "editors") to_review_enabled = asbool(settings.get("signer.to_review_enabled", False)) group_check_enabled = asbool(settings.get("signer.group_check_enabled", False)) config.registry.signers = {} for key, resource in resources.items(): # Load the signers associated to each resource. dotted_location, prefix = _signer_dotted_location(settings, resource) signer_module = config.maybe_dotted(dotted_location) backend = signer_module.load_from_settings(settings, prefix) config.registry.signers[key] = backend # Load the setttings associated to each resource. prefix = "{source[bucket]}_{source[collection]}".format(**resource) for setting in ("reviewers_group", "editors_group", "to_review_enabled", "group_check_enabled"): value = settings.get("signer.%s.%s" % (prefix, setting), settings.get("signer.%s_%s" % (prefix, setting))) if value is not None: resource[setting] = value # Expose the capabilities in the root endpoint. message = "Digital signatures for integrity and authenticity of records." docs = "https://github.com/Kinto/kinto-signer#kinto-signer" config.add_api_capability("signer", message, docs, version=__version__, resources=resources.values(), to_review_enabled=to_review_enabled, group_check_enabled=group_check_enabled, editors_group=editors_group, reviewers_group=reviewers_group) config.add_subscriber( functools.partial(listeners.set_work_in_progress_status, resources=resources), ResourceChanged, for_resources=('record',)) config.add_subscriber( functools.partial(listeners.check_collection_status, resources=resources, to_review_enabled=to_review_enabled, group_check_enabled=group_check_enabled, editors_group=editors_group, reviewers_group=reviewers_group), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.check_collection_tracking, resources=resources), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) sign_data_listener = functools.partial(listeners.sign_collection_data, resources=resources) # If StatsD is enabled, monitor execution time of listener. if config.registry.statsd: # Due to https://github.com/jsocol/pystatsd/issues/85 for attr in ('__module__', '__name__'): origin = getattr(listeners.sign_collection_data, attr) setattr(sign_data_listener, attr, origin) statsd_client = config.registry.statsd key = 'plugins.signer' sign_data_listener = statsd_client.timer(key)(sign_data_listener) config.add_subscriber( sign_data_listener, ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',))
def test_updater_does_not_fail_when_payload_is_inconsistent(self): # This happens with events on default bucket for kinto < 3.3 evt = mock.MagicMock(payload={"action": "update", "subpath": "collections/boom"}) sign_collection_data(evt, resources=utils.parse_resources("a/b -> c/d"), to_review_enabled=True)
def includeme(config): # We import stuff here, so that kinto-signer can be installed with `--no-deps` # and used without having this Pyramid ecosystem installed. import transaction from kinto.core.events import ACTIONS, ResourceChanged from pyramid.exceptions import ConfigurationError from pyramid.events import NewRequest from pyramid.settings import asbool from kinto_signer.signer import heartbeat from kinto_signer import utils from kinto_signer import listeners # Register heartbeat to check signer integration. config.registry.heartbeats['signer'] = heartbeat settings = config.get_settings() # Check source and destination resources are configured. raw_resources = settings.get('signer.resources') if raw_resources is None: error_msg = "Please specify the kinto.signer.resources setting." raise ConfigurationError(error_msg) resources = utils.parse_resources(raw_resources) # Expand the resources with the ones that come from per-bucket resources # and have specific settings. # For example, consider the case where resource is ``/buckets/dev -> /buckets/prod`` # and there is a setting ``signer.dev.recipes.signer_backend = foo`` output_resources = resources.copy() for key, resource in resources.items(): # If collection is not None, there is nothing to expand :) if resource['source']['collection'] is not None: continue bid = resource['source']['bucket'] # Match setting names like signer.stage.specific.autograph.hawk_id matches = [(v, re.search(r'signer\.{0}\.([^\.]+)\.(.+)'.format(bid), k)) for k, v in settings.items()] found = [(v, m.group(1), m.group(2)) for (v, m) in matches if m] # Expand the list of resources with the ones that contain collection # specific settings. for setting_value, cid, setting_name in found: signer_key = "/buckets/{0}/collections/{1}".format(bid, cid) if signer_key not in output_resources: specific = copy.deepcopy(resource) specific["source"]["collection"] = cid specific["destination"]["collection"] = cid if "preview" in specific: specific["preview"]["collection"] = cid output_resources[signer_key] = specific output_resources[signer_key][setting_name] = setting_value resources = output_resources # Determine which are the settings that apply to all buckets/collections. defaults = { "reviewers_group": "reviewers", "editors_group": "editors", "to_review_enabled": False, "group_check_enabled": False, } global_settings = {} for setting in listeners.REVIEW_SETTINGS: value = settings.get("signer.%s" % setting, defaults[setting]) if setting.endswith("_enabled"): value = asbool(value) global_settings[setting] = value # For each resource that is configured, we determine what signer is # configured and what are the review settings. # Note: the `resource` values are mutated in place. config.registry.signers = {} for signer_key, resource in resources.items(): server_wide = 'signer.' bucket_wide = 'signer.{bucket}.'.format(**resource['source']) prefixes = [bucket_wide, server_wide] per_bucket_config = resource['source']['collection'] is None if not per_bucket_config: collection_wide = 'signer.{bucket}.{collection}.'.format(**resource['source']) deprecated = 'signer.{bucket}_{collection}.'.format(**resource['source']) prefixes = [collection_wide, deprecated] + prefixes # Instantiates the signers associated to this resource. dotted_location = utils.get_first_matching_setting('signer_backend', settings, prefixes, default=DEFAULT_SIGNER) signer_module = config.maybe_dotted(dotted_location) backend = signer_module.load_from_settings(settings, prefixes=prefixes) config.registry.signers[signer_key] = backend # Load the setttings associated to each resource. for setting in listeners.REVIEW_SETTINGS: # Per collection/bucket: value = utils.get_first_matching_setting(setting, settings, prefixes, default=global_settings[setting]) if setting.endswith("_enabled"): value = asbool(value) # Resolve placeholder with source info. if setting.endswith("_group"): # If configured per bucket, then we leave the placeholder. # It will be resolved in listeners during group checking and # by Kinto-Admin when matching user groups with info from capabilities. collection_id = resource['source']['collection'] or "{collection_id}" try: value = value.format(bucket_id=resource['source']['bucket'], collection_id=collection_id) except KeyError as e: raise ConfigurationError("Unknown group placeholder %s" % e) # Only expose if relevant. if value != global_settings[setting]: resource[setting] = value else: resource.pop(setting, None) # Expose the capabilities in the root endpoint. message = "Digital signatures for integrity and authenticity of records." docs = "https://github.com/Kinto/kinto-signer#kinto-signer" config.add_api_capability("signer", message, docs, version=__version__, resources=resources.values(), **global_settings) config.add_subscriber( functools.partial(listeners.set_work_in_progress_status, resources=resources), ResourceChanged, for_resources=('record',)) config.add_subscriber( functools.partial(listeners.check_collection_status, resources=resources, **global_settings), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.check_collection_tracking, resources=resources), ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.create_editors_reviewers_groups, resources=resources, editors_group=global_settings["editors_group"], reviewers_group=global_settings["reviewers_group"]), ResourceChanged, for_actions=(ACTIONS.CREATE,), for_resources=('collection',)) config.add_subscriber( functools.partial(listeners.cleanup_preview_destination, resources=resources), ResourceChanged, for_actions=(ACTIONS.DELETE,), for_resources=('collection',)) sign_data_listener = functools.partial(listeners.sign_collection_data, resources=resources, **global_settings) # If StatsD is enabled, monitor execution time of listener. if config.registry.statsd: # Due to https://github.com/jsocol/pystatsd/issues/85 for attr in ('__module__', '__name__'): origin = getattr(listeners.sign_collection_data, attr) setattr(sign_data_listener, attr, origin) statsd_client = config.registry.statsd key = 'plugins.signer' sign_data_listener = statsd_client.timer(key)(sign_data_listener) config.add_subscriber( sign_data_listener, ResourceChanged, for_actions=(ACTIONS.CREATE, ACTIONS.UPDATE), for_resources=('collection',)) def on_new_request(event): """Send the kinto-signer events in the before commit hook. This allows database operations done in subscribers to be automatically committed or rolledback. """ # Since there is one transaction per batch, ignore subrequests. if hasattr(event.request, 'parent'): return current = transaction.get() current.addBeforeCommitHook(listeners.send_signer_events, args=(event,)) config.add_subscriber(on_new_request, NewRequest)