Beispiel #1
0
 def test_missing_semicolumn_raises_an_exception(self):
     raw_resources = """
     foo
     bar
     """
     with pytest.raises(ConfigurationError):
         utils.parse_resources(raw_resources)
Beispiel #2
0
 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)
Beispiel #3
0
 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)
Beispiel #4
0
 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)
Beispiel #5
0
 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)
Beispiel #6
0
    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
Beispiel #8
0
 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
Beispiel #9
0
 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
Beispiel #10
0
 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"},
         }
     }
Beispiel #11
0
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',)
    )
Beispiel #12
0
 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
Beispiel #14
0
 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'
             }
         }
     }
Beispiel #15
0
    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
Beispiel #16
0
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)
Beispiel #17
0
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)
Beispiel #18
0
 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)
Beispiel #19
0
    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)
Beispiel #20
0
 def test_missing_arrow_raises_an_exception(self):
     raw_resources = """
     foo bar
     """
     with pytest.raises(ConfigurationError):
         utils.parse_resources(raw_resources)
Beispiel #21
0
 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)
Beispiel #22
0
 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
Beispiel #24
0
 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)
Beispiel #25
0
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)
Beispiel #27
0
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)