Exemple #1
0
    def apply_changes(self, obj, requested_changes):
        """Merge `changes` into `object` fields.

        .. note::

            This is used in the context of PATCH only.

        Override this to control field changes at object level, for example:

        .. code-block:: python

            def apply_changes(self, obj, requested_changes):
                # Ignore value change if inferior
                if object['position'] > changes.get('position', -1):
                    changes.pop('position', None)
                return super().apply_changes(obj, requested_changes)

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
            if result does not comply with resource schema.

        :returns: the new object with `changes` applied.
        :rtype: tuple
        """
        if self._is_json_patch:
            try:
                applied_changes = apply_json_patch(obj, requested_changes)["data"]
                updated = {**applied_changes}
            except ValueError as e:
                error_details = {
                    "location": "body",
                    "description": f"JSON Patch operation failed: {e}",
                }
                raise_invalid(self.request, **error_details)

        else:
            applied_changes = {**requested_changes}
            updated = {**obj}

            # recursive patch and remove field if null attribute is passed (RFC 7396)
            if self._is_merge_patch:
                recursive_update_dict(updated, applied_changes, ignores=(None,))
            else:
                updated.update(**applied_changes)

        for field, value in applied_changes.items():
            has_changed = obj.get(field, value) != value
            if self.schema.is_readonly(field) and has_changed:
                error_details = {"name": field, "description": f"Cannot modify {field}"}
                raise_invalid(self.request, **error_details)

        try:
            validated = self.schema().deserialize(updated)
        except colander.Invalid as e:
            # Transform the errors we got from colander into Cornice errors.
            # We could not rely on Service schema because the object should be
            # validated only once the changes are applied
            for field, error in e.asdict().items():  # pragma: no branch
                raise_invalid(self.request, name=field, description=error)

        return validated, applied_changes
Exemple #2
0
    def process_record(self, new, old=None):
        """Read permissions from request body, and in the case of ``PUT`` every
        existing ACE is removed (using empty list).
        """
        new = super().process_record(new, old)

        # patch is specified as a list of of operations (RFC 6902)

        payload = self.request.validated['body']

        if self._is_json_patch:
            permissions = apply_json_patch(old, payload)['permissions']

        elif self._is_merge_patch:
            existing = old or {}
            permissions = existing.get('__permissions__', {})
            recursive_update_dict(permissions,
                                  payload.get('permissions', {}),
                                  ignores=(None,))

        else:
            permissions = {k: v for k, v in payload.get('permissions', {}).items()
                           if v is not None}

        annotated = {**new}

        if permissions:
            is_put = (self.request.method.lower() == 'put')
            if is_put or self._is_merge_patch:
                # Remove every existing ACEs using empty lists.
                for perm in self.permissions:
                    permissions.setdefault(perm, [])
            annotated[self.model.permissions_field] = permissions

        return annotated
Exemple #3
0
    def process_record(self, new, old=None):
        """Read permissions from request body, and in the case of ``PUT`` every
        existing ACE is removed (using empty list).
        """
        new = super().process_record(new, old)

        # patch is specified as a list of of operations (RFC 6902)

        payload = self.request.validated['body']

        if self._is_json_patch:
            permissions = apply_json_patch(old, payload)['permissions']

        elif self._is_merge_patch:
            existing = old or {}
            permissions = existing.get('__permissions__', {})
            recursive_update_dict(permissions,
                                  payload.get('permissions', {}),
                                  ignores=(None,))

        else:
            permissions = {k: v for k, v in payload.get('permissions', {}).items()
                           if v is not None}

        annotated = {**new}

        if permissions:
            is_put = (self.request.method.lower() == 'put')
            if is_put or self._is_merge_patch:
                # Remove every existing ACEs using empty lists.
                for perm in self.permissions:
                    permissions.setdefault(perm, [])
            annotated[self.model.permissions_field] = permissions

        return annotated
Exemple #4
0
    def apply_changes(self, record, changes):
        """Merge `changes` into `record` fields.

        .. note::

            This is used in the context of PATCH only.

        Override this to control field changes at record level, for example:

        .. code-block:: python

            def apply_changes(self, record, changes):
                # Ignore value change if inferior
                if record['position'] > changes.get('position', -1):
                    changes.pop('position', None)
                return super(MyResource, self).apply_changes(record, changes)

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
            if result does not comply with resource schema.

        :returns: the new record with `changes` applied.
        :rtype: dict
        """
        for field, value in changes.items():
            has_changed = record.get(field, value) != value
            if self.mapping.is_readonly(field) and has_changed:
                error_details = {
                    'name': field,
                    'description': 'Cannot modify {0}'.format(field)
                }
                raise_invalid(self.request, **error_details)

        updated = record.copy()

        # recursive patch and remove field if null attribute is passed (RFC 7396)
        content_type = str(self.request.headers.get('Content-Type'))
        if content_type == 'application/merge-patch+json':
            recursive_update_dict(updated, changes, ignores=[None])
        else:
            updated.update(**changes)

        try:
            return self.mapping.deserialize(updated)
        except colander.Invalid as e:
            # Transform the errors we got from colander into Cornice errors.
            # We could not rely on Service schema because the record should be
            # validated only once the changes are applied
            for field, error in e.asdict().items():
                raise_invalid(self.request, name=field, description=error)
Exemple #5
0
def swagger_view(request):

    # Only build json once
    try:
        return swagger_view.__json__
    except AttributeError:
        pass

    settings = request.registry.settings

    # Base swagger spec
    files = [
        settings.get('swagger_file', ''),  # From config
        os.path.join(ORIGIN, 'swagger.yaml'),  # Relative to the package root
        os.path.join(HERE, 'swagger.yaml')  # Relative to this file.
    ]

    files = [f for f in files if os.path.exists(f)]

    # Get first file that exists
    if files:
        files = files[:1]
    else:
        raise httpexceptions.HTTPNotFound()

    # Plugin swagger extensions
    includes = aslist(settings.get('includes', ''))
    for app in includes:
        f = pkg_resources.resource_filename(app, 'swagger.yaml')
        if os.path.exists(f):
            files.append(f)

    swagger_view.__json__ = {}

    # Read and merge files
    for path in files:
        abs_path = os.path.abspath(path)
        with open(abs_path) as f:
            spec = yaml.safe_load(f)
            recursive_update_dict(swagger_view.__json__, spec)

    # Update instance fields
    info = dict(title=settings['project_name'],
                version=settings['http_api_version'])

    schemes = [settings.get('http_scheme') or 'http']

    security_defs = swagger_view.__json__.get('securityDefinitions', {})

    # BasicAuth is a non extension capability, so we should check it from config
    if 'basicauth' in aslist(settings.get('multiauth.policies', '')):
        basicauth = {
            'type': 'basic',
            'description': 'HTTP Basic Authentication.'
        }
        security_defs['basicAuth'] = basicauth

    # Security options are JSON objects with a single key
    security = swagger_view.__json__.get('security', [])
    security_names = [next(iter(security_def)) for security_def in security]

    # include securityDefinitions that are not on default security options
    for name, prop in security_defs.items():
        security_def = {name: prop.get('scopes', {}).keys()}
        if name not in security_names:
            security.append(security_def)

    data = dict(info=info,
                host=request.host,
                basePath=request.path.replace(swagger.path, ''),
                schemes=schemes,
                securityDefinitions=security_defs,
                security=security)

    recursive_update_dict(swagger_view.__json__, data)

    return swagger_view.__json__
Exemple #6
0
 def test_merge_non_dict(self):
     a = {}
     recursive_update_dict(a, 1)
     self.assertEqual(a, {})
Exemple #7
0
 def test_merge(self):
     a = {}
     recursive_update_dict(a, {"b": {"c": 1}, "d": 2})
     self.assertEqual(a["b"]["c"], 1)
     self.assertEqual(a["d"], 2)
Exemple #8
0
    def process_object(self, new, old=None):
        """Hook for processing objects before they reach storage, to introduce
        specific logics on fields for example.

        .. code-block:: python

            def process_object(self, new, old=None):
                new = super().process_object(new, old)
                version = old['version'] if old else 0
                new['version'] = version + 1
                return new

        Or add extra validation based on request:

        .. code-block:: python

            from kinto.core.errors import raise_invalid

            def process_object(self, new, old=None):
                new = super().process_object(new, old)
                if new['browser'] not in request.headers['User-Agent']:
                    raise_invalid(self.request, name='browser', error='Wrong')
                return new

        :param dict new: the validated object to be created or updated.
        :param dict old: the old object to be updated,
            ``None`` for creation endpoints.

        :returns: the processed object.
        :rtype: dict
        """
        modified_field = self.model.modified_field
        new_last_modified = new.get(modified_field)

        # Drop the new last_modified if it is not an integer.
        is_integer = isinstance(new_last_modified, int)
        if not is_integer:
            new.pop(modified_field, None)
            new_last_modified = None

        # Drop the new last_modified if lesser or equal to the old one.
        is_less_or_equal = (
            new_last_modified and old is not None and new_last_modified <= old[modified_field]
        )
        if is_less_or_equal:
            new.pop(modified_field, None)

        # patch is specified as a list of of operations (RFC 6902)

        payload = self.request.validated["body"]

        if self._is_json_patch:
            permissions = apply_json_patch(old, payload)["permissions"]

        elif self._is_merge_patch:
            existing = old or {}
            permissions = existing.get("__permissions__", {})
            recursive_update_dict(permissions, payload.get("permissions", {}), ignores=(None,))

        else:
            permissions = {
                k: v for k, v in payload.get("permissions", {}).items() if v is not None
            }

        annotated = {**new}

        if permissions:
            is_put = self.request.method.lower() == "put"
            if is_put or self._is_merge_patch:
                # Remove every existing ACEs using empty lists.
                for perm in self.permissions:
                    permissions.setdefault(perm, [])
            annotated[self.model.permissions_field] = permissions

        return annotated
Exemple #9
0
 def test_merge(self):
     a = {}
     recursive_update_dict(a, {'b': {'c': 1}, 'd': 2})
     self.assertEqual(a['b']['c'], 1)
     self.assertEqual(a['d'], 2)
Exemple #10
0
 def test_merge_non_dict(self):
     a = {}
     recursive_update_dict(a, 1)
     self.assertEqual(a, {})
Exemple #11
0
 def test_merge(self):
     a = {}
     recursive_update_dict(a, {'b': {'c': 1}, 'd': 2})
     self.assertEqual(a['b']['c'], 1)
     self.assertEqual(a['d'], 2)
Exemple #12
0
    def apply_changes(self, record, requested_changes):
        """Merge `changes` into `record` fields.

        .. note::

            This is used in the context of PATCH only.

        Override this to control field changes at record level, for example:

        .. code-block:: python

            def apply_changes(self, record, requested_changes):
                # Ignore value change if inferior
                if record['position'] > changes.get('position', -1):
                    changes.pop('position', None)
                return super().apply_changes(record, requested_changes)

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
            if result does not comply with resource schema.

        :returns: the new record with `changes` applied.
        :rtype: tuple
        """
        if self._is_json_patch:
            try:
                applied_changes = apply_json_patch(record, requested_changes)['data']
                updated = {**applied_changes}
            except ValueError as e:
                error_details = {
                    'location': 'body',
                    'description': 'JSON Patch operation failed: {}'.format(e)
                }
                raise_invalid(self.request, **error_details)

        else:
            applied_changes = {**requested_changes}
            updated = {**record}

            # recursive patch and remove field if null attribute is passed (RFC 7396)
            if self._is_merge_patch:
                recursive_update_dict(updated, applied_changes, ignores=(None,))
            else:
                updated.update(**applied_changes)

        for field, value in applied_changes.items():
            has_changed = record.get(field, value) != value
            if self.schema.is_readonly(field) and has_changed:
                error_details = {
                    'name': field,
                    'description': 'Cannot modify {}'.format(field)
                }
                raise_invalid(self.request, **error_details)

        try:
            validated = self.schema().deserialize(updated)
        except colander.Invalid as e:
            # Transform the errors we got from colander into Cornice errors.
            # We could not rely on Service schema because the record should be
            # validated only once the changes are applied
            for field, error in e.asdict().items():  # pragma: no branch
                raise_invalid(self.request, name=field, description=error)

        return validated, applied_changes
Exemple #13
0
 def test_merge(self):
     a = {}
     recursive_update_dict(a, {"b": {"c": 1}, "d": 2})
     self.assertEqual(a["b"]["c"], 1)
     self.assertEqual(a["d"], 2)