예제 #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
예제 #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
예제 #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
예제 #4
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)
        if self._is_json_patch:
            changes = self.request.validated['body']
            permissions = apply_json_patch(old, changes)['permissions']
        else:
            permissions = self.request.validated['body'].get('permissions', {})

        annotated = {**new}

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

        return annotated
예제 #5
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}

            content_type = str(self.request.headers.get('Content-Type')).lower()
            # recursive patch and remove field if null attribute is passed (RFC 7396)
            if content_type == 'application/merge-patch+json':
                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
예제 #6
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
예제 #7
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