Exemple #1
0
    def _validate_unique_template(self, unique, field, value):
        """Check that value is unique globally or to current user.

        In case 'is_public' is false within document it will check for unique within
        docs with same 'user' value.

        Otherwise it will check for unique within docs without any 'user' value.
        """
        original = self._original_document or {}
        update = self.document or {}

        is_public = update.get('is_public', original.get('is_public', None))
        template_name = update.get('template_name',
                                   original.get('template_name', None))

        if is_public:
            query = {'is_public': True}
        else:
            _, auth_value = auth_field_and_value(self.resource)
            query = {'user': auth_value, 'is_public': False}

        query['template_name'] = re.compile(
            '^{}$'.format(re.escape(template_name.strip())), re.IGNORECASE)

        if self._id:
            id_field = config.DOMAIN[self.resource]['id_field']
            query[id_field] = {'$ne': self._id}

        if superdesk.get_resource_service(self.resource).find_one(req=None,
                                                                  **query):
            self._error(field, "Template Name is not unique")
Exemple #2
0
    def _validate_unique_template(self, unique, field, value):
        """
        {'type': 'boolean'}
        """
        original = self.persisted_document or {}
        update = self.document or {}

        is_public = update.get("is_public", original.get("is_public", None))
        template_name = update.get("template_name",
                                   original.get("template_name", None))

        if is_public:
            query = {"is_public": True}
        else:
            _, auth_value = auth_field_and_value(self.resource)
            query = {"user": auth_value, "is_public": False}

        query["template_name"] = re.compile(
            "^{}$".format(re.escape(template_name.strip())), re.IGNORECASE)

        if self.document_id:
            id_field = config.DOMAIN[self.resource]["id_field"]
            query[id_field] = {"$ne": self.document_id}

        if superdesk.get_resource_service(self.resource).find_one(req=None,
                                                                  **query):
            self._error(field, "Template Name is not unique")
Exemple #3
0
    def _validate_unique_template(self, unique, field, value):
        """Check that value is unique globally or to current user.

        In case 'is_public' is false within document it will check for unique within
        docs with same 'user' value.

        Otherwise it will check for unique within docs without any 'user' value.
        """
        original = self._original_document or {}
        update = self.document or {}

        is_public = update.get('is_public', original.get('is_public', None))
        template_name = update.get('template_name', original.get('template_name', None))

        if is_public:
            query = {'is_public': True}
        else:
            _, auth_value = auth_field_and_value(self.resource)
            query = {'user': auth_value, 'is_public': False}

        query['template_name'] = re.compile('^{}$'.format(re.escape(template_name.strip())), re.IGNORECASE)

        if self._id:
            id_field = config.DOMAIN[self.resource]['id_field']
            query[id_field] = {'$ne': self._id}

        if superdesk.get_resource_service(self.resource).find_one(req=None, **query):
            self._error(field, "Template Name is not unique")
Exemple #4
0
    def _validate_unique_to_user(self, unique, field, value):
        """ {'type': 'boolean'} """
        auth_field, auth_value = auth_field_and_value(self.resource)

        # if an auth value has been set for this request, then make sure it is
        # taken into account when checking for value uniqueness.
        query = {auth_field: auth_value} if auth_field else {}

        self._is_value_unique(unique, field, value, query)
Exemple #5
0
    def _validate_unique_to_user(self, unique, field, value):
        """ {'type': 'boolean'} """
        auth_field, auth_value = auth_field_and_value(self.resource)

        # if an auth value has been set for this request, then make sure it is
        # taken into account when checking for value uniqueness.
        query = {auth_field: auth_value} if auth_field else {}

        self._is_value_unique(unique, field, value, query)
    def _validate_unique_to_user(self, unique, field, value):
        """
        {'type': 'boolean'}
        """
        doc = getattr(self, "document", getattr(self, "original_document", {}))

        if "user" in doc:
            _, auth_value = auth_field_and_value(self.resource)
            query = {"user": auth_value}
        else:
            query = {"user": {"$exists": False}}

        self._is_value_unique(unique, field, value, query)
Exemple #7
0
    def _validate_unique_to_user(self, unique, field, value):
        """
        {'type': 'boolean'}
        """
        doc = getattr(self, 'document', getattr(self, 'original_document', {}))

        if 'user' in doc:
            _, auth_value = auth_field_and_value(self.resource)
            query = {'user': auth_value}
        else:
            query = {'user': {'$exists': False}}

        self._is_value_unique(unique, field, value, query)
    def _validate_unique_to_user(self, unique, field, value):
        """ Validates that a value is unique to the active user. Active user is
        the user authenticated for current request. See #646.

        .. versionadded: 0.6
        """

        auth_field, auth_value = auth_field_and_value(self.resource)

        # if an auth value has been set for this request, then make sure it is
        # taken into account when checking for value uniqueness.
        query = {auth_field: auth_value} if auth_field else {}

        self._is_value_unique(unique, field, value, query)
Exemple #9
0
    def _validate_unique_to_user(self, unique, field, value):
        """ Validates that a value is unique to the active user. Active user is
        the user authenticated for current request. See #646.

        .. versionadded: 0.6
        """

        auth_field, auth_value = auth_field_and_value(self.resource)

        # if an auth value has been set for this request, then make sure it is
        # taken into account when checking for value uniqueness.
        query = {auth_field: auth_value} if auth_field else {}

        self._is_value_unique(unique, field, value, query)
Exemple #10
0
    def _validate_unique_to_user(self, unique, field, value):
        """Check that value is unique globally or to current user.

        In case 'user' is set within document it will check for unique within
        docs with same 'user' value.

        Otherwise it will check for unique within docs without any 'user' value.
        """
        doc = getattr(self, 'document', getattr(self, 'original_document', {}))

        if 'user' in doc:
            _, auth_value = auth_field_and_value(self.resource)
            query = {'user': auth_value}
        else:
            query = {'user': {'$exists': False}}

        self._is_value_unique(unique, field, value, query)
    def _validate_unique_to_user(self, unique, field, value):
        """Check that value is unique globally or to current user.

        In case 'user' is set within document it will check for unique within
        docs with same 'user' value.

        Otherwise it will check for unique within docs without any 'user' value.
        """
        doc = getattr(self, 'document', getattr(self, 'original_document', {}))

        if 'user' in doc:
            _, auth_value = auth_field_and_value(self.resource)
            query = {'user': auth_value}
        else:
            query = {'user': {'$exists': False}}

        self._is_value_unique(unique, field, value, query)
Exemple #12
0
    def _validate_unique_template(self, unique, field, value):
        """ {'type': 'boolean'} """
        original = self._original_document or {}
        update = self.document or {}

        is_public = update.get('is_public', original.get('is_public', None))
        template_name = update.get('template_name', original.get('template_name', None))

        if is_public:
            query = {'is_public': True}
        else:
            _, auth_value = auth_field_and_value(self.resource)
            query = {'user': auth_value, 'is_public': False}

        query['template_name'] = re.compile('^{}$'.format(re.escape(template_name.strip())), re.IGNORECASE)

        if self._id:
            id_field = config.DOMAIN[self.resource]['id_field']
            query[id_field] = {'$ne': self._id}

        if eden.get_resource_service(self.resource).find_one(req=None, **query):
            self._error(field, "Template Name is not unique")
Exemple #13
0
    def _datasource_ex(self, resource, query=None, client_projection=None,
                       client_sort=None):
        """ Returns both db collection and exact query (base filter included)
        to which an API resource refers to.

        .. versionchanged:: 0.4
           Always return required/auto fields (issue 282.)

        .. versionchanged:: 0.3
           Field exclusion support in client projections.
           Honor auth_field even when client query is missing.
           Only inject auth_field in queries when we are not creating new
           documents.
           'auth_field' and 'request_auth_value' fetching is now delegated to
           auth.auth_field_and value().

        .. versionchanged:: 0.2
           Difference between resource and item endpoints is now determined
           by the presence of a '|' in request.endpoint.
           Support for 'default_sort'.

        .. versionchanged:: 0.1.1
           auth.request_auth_value is now used to store the auth_field value.

        .. versionchanged:: 0.1.0
           Calls `combine_queries` to merge query and filter_
           Updated logic performing `auth_field` check

        .. versionchanged:: 0.0.9
           Storing self.app.auth.userid in auth_field when 'user-restricted
           resource access' is enabled.
           Support for Python 3.3.

        .. versionchanged:: 0.0.6
           'auth_username_field' is injected even in empty queries.
           Projection queries ('?projection={"name": 1}')

        .. versionchanged:: 0.0.5
           Support for 'user-restricted resource access'.

        .. versionadded:: 0.0.4
        """

        datasource, filter_, projection_, sort_ = self._datasource(resource)

        if client_sort:
            sort = client_sort
        else:
            # default sort is activated only if 'sorting' is enabled for the
            # resource.
            # TODO Consider raising a validation error on startup instead?
            sort = sort_ if sort_ and config.DOMAIN[resource]['sorting'] else \
                None

        if filter_:
            if query:
                # Can't just dump one set of query operators into another
                # e.g. if the dataset contains a custom datasource pattern
                #   'filter': {'username': {'$exists': True}}
                # and we try to filter on the field `username`,
                # which is correct?

                # Solution: call the db driver `combine_queries` operation
                # which will apply db-specific syntax to produce the
                # intersection of the two queries
                query = self.combine_queries(query, filter_)
            else:
                query = filter_

        fields = projection_
        if client_projection:
            # only allow fields which are included with the standard projection
            # for the resource (avoid sniffing of private fields)
            keep_fields = auto_fields(resource)
            if 0 not in client_projection.values():
                # inclusive projection - all values are 0 unless spec. or auto
                fields = dict([(field, field in keep_fields) for field in
                               fields.keys()])
            for field, value in client_projection.items():
                field_base = field.split('.')[0]
                if field_base not in keep_fields and field_base in fields:
                    fields[field] = value
            fields = dict([(field, 1) for field, value in fields.items() if
                           value])

        # If the current HTTP method is in `public_methods` or
        # `public_item_methods`, skip the `auth_field` check

        # Only inject the auth_field in the query when not creating new
        # documents.
        if request and request.method not in ('POST', 'PUT'):
            auth_field, request_auth_value = auth_field_and_value(resource)
            if auth_field and request.authorization and request_auth_value:
                if query:
                    # If the auth_field *replaces* a field in the query,
                    # and the values are /different/, deny the request
                    # This prevents the auth_field condition from
                    # overwriting the query (issue #77)
                    auth_field_in_query = \
                        self.app.data.query_contains_field(query, auth_field)
                    if auth_field_in_query and \
                            self.app.data.get_value_from_query(
                                query, auth_field) != request_auth_value:
                        abort(401, description=debug_error_message(
                            'Incompatible User-Restricted Resource request. '
                            'Request was for "%s"="%s" but `auth_field` '
                            'requires "%s"="%s".' % (
                                auth_field,
                                self.app.data.get_value_from_query(
                                    query, auth_field),
                                auth_field,
                                request_auth_value)
                        ))
                    else:
                        query = self.app.data.combine_queries(
                            query, {auth_field: request_auth_value}
                        )
                else:
                    query = {auth_field: request_auth_value}
        return datasource, query, fields, sort
Exemple #14
0
    def _datasource_ex(self, resource, query=None, client_projection=None,
                       client_sort=None):
        """ Returns both db collection and exact query (base filter included)
        to which an API resource refers to.

        .. versionchanged:: 0.4
           Always return required/auto fields (issue 282.)

        .. versionchanged:: 0.3
           Field exclusion support in client projections.
           Honor auth_field even when client query is missing.
           Only inject auth_field in queries when we are not creating new
           documents.
           'auth_field' and 'request_auth_value' fetching is now delegated to
           auth.auth_field_and value().

        .. versionchanged:: 0.2
           Difference between resource and item endpoints is now determined
           by the presence of a '|' in request.endpoint.
           Support for 'default_sort'.

        .. versionchanged:: 0.1.1
           auth.request_auth_value is now used to store the auth_field value.

        .. versionchanged:: 0.1.0
           Calls `combine_queries` to merge query and filter_
           Updated logic performing `auth_field` check

        .. versionchanged:: 0.0.9
           Storing self.app.auth.userid in auth_field when 'user-restricted
           resource access' is enabled.
           Support for Python 3.3.

        .. versionchanged:: 0.0.6
           'auth_username_field' is injected even in empty queries.
           Projection queries ('?projection={"name": 1}')

        .. versionchanged:: 0.0.5
           Support for 'user-restricted resource access'.

        .. versionadded:: 0.0.4
        """

        datasource, filter_, projection_, sort_ = self._datasource(resource)

        if client_sort:
            sort = client_sort
        else:
            # default sort is activated only if 'sorting' is enabled for the
            # resource.
            # TODO Consider raising a validation error on startup instead?
            sort = sort_ if sort_ and config.DOMAIN[resource]['sorting'] else \
                None

        if filter_:
            if query:
                # Can't just dump one set of query operators into another
                # e.g. if the dataset contains a custom datasource pattern
                #   'filter': {'username': {'$exists': True}}
                # and we try to filter on the field `username`,
                # which is correct?

                # Solution: call the db driver `combine_queries` operation
                # which will apply db-specific syntax to produce the
                # intersection of the two queries
                query = self.combine_queries(query, filter_)
            else:
                query = filter_

        fields = projection_
        keep_fields = auto_fields(resource)
        if client_projection:
            # only allow fields which are included with the standard projection
            # for the resource (avoid sniffing of private fields)
            if 0 in client_projection.values():
                # exclusive projection - all values are 1 unless specified
                for field, value in client_projection.items():
                    if value == 0 and value not in keep_fields and \
                            field in fields:
                        del fields[field]
            else:
                # inclusive projection - all values are 0 unless spec. or auto
                for field in list(fields.keys()):
                    if field not in client_projection and \
                            field not in keep_fields:
                        del fields[field]

        # If the current HTTP method is in `public_methods` or
        # `public_item_methods`, skip the `auth_field` check

        # Only inject the auth_field in the query when not creating new
        # documents.
        if request and request.method not in ('POST', 'PUT'):
            auth_field, request_auth_value = auth_field_and_value(resource)
            if auth_field and request.authorization and request_auth_value:
                if query:
                    # If the auth_field *replaces* a field in the query,
                    # and the values are /different/, deny the request
                    # This prevents the auth_field condition from
                    # overwriting the query (issue #77)
                    auth_field_in_query = \
                        self.app.data.query_contains_field(query, auth_field)
                    if auth_field_in_query and \
                            self.app.data.get_value_from_query(
                                query, auth_field) != request_auth_value:
                        abort(401, description=debug_error_message(
                            'Incompatible User-Restricted Resource request. '
                            'Request was for "%s"="%s" but `auth_field` '
                            'requires "%s"="%s".' % (
                                auth_field,
                                self.app.data.get_value_from_query(
                                    query, auth_field),
                                auth_field,
                                request_auth_value)
                        ))
                    else:
                        query = self.app.data.combine_queries(
                            query, {auth_field: request_auth_value}
                        )
                else:
                    query = {auth_field: request_auth_value}
        return datasource, query, fields, sort
Exemple #15
0
    def _datasource_ex(self,
                       resource,
                       query=None,
                       client_projection=None,
                       client_sort=None,
                       check_auth_value=True,
                       force_auth_field_projection=False):
        """ Returns both db collection and exact query (base filter included)
        to which an API resource refers to.

        .. versionchanged:: 0.5.2
           Make User Restricted Resource Access work with HMAC Auth too.

        .. versionchanged:: 0.5
           Let client projection work when 'allow_unknown' is active (#497).

        .. versionchanged:: 0.4
           Always return required/auto fields (issue 282.)

        .. versionchanged:: 0.3
           Field exclusion support in client projections.
           Honor auth_field even when client query is missing.
           Only inject auth_field in queries when we are not creating new
           documents.
           'auth_field' and 'request_auth_value' fetching is now delegated to
           auth.auth_field_and value().

        .. versionchanged:: 0.2
           Difference between resource and item endpoints is now determined
           by the presence of a '|' in request.endpoint.
           Support for 'default_sort'.

        .. versionchanged:: 0.1.1
           auth.request_auth_value is now used to store the auth_field value.

        .. versionchanged:: 0.1.0
           Calls `combine_queries` to merge query and filter_
           Updated logic performing `auth_field` check

        .. versionchanged:: 0.0.9
           Storing self.app.auth.userid in auth_field when 'user-restricted
           resource access' is enabled.
           Support for Python 3.3.

        .. versionchanged:: 0.0.6
           'auth_username_field' is injected even in empty queries.
           Projection queries ('?projection={"name": 1}')

        .. versionchanged:: 0.0.5
           Support for 'user-restricted resource access'.

        .. versionadded:: 0.0.4
        """

        datasource, filter_, projection_, sort_ = self.datasource(resource)
        if client_sort:
            sort = client_sort
        else:
            # default sort is activated only if 'sorting' is enabled for the
            # resource.
            # TODO Consider raising a validation error on startup instead?
            sort = sort_ if sort_ and config.DOMAIN[resource]['sorting'] else \
                None

        if filter_:
            if query:
                # Can't just dump one set of query operators into another
                # e.g. if the dataset contains a custom datasource pattern
                #   'filter': {'username': {'$exists': True}}
                # and we try to filter on the field `username`,
                # which is correct?

                # Solution: call the db driver `combine_queries` operation
                # which will apply db-specific syntax to produce the
                # intersection of the two queries
                query = self.combine_queries(query, filter_)
            else:
                query = filter_

        fields = projection_
        if client_projection:
            if projection_:
                # only allow fields which are included with the standard
                # projection for the resource (avoid sniffing of private
                # fields)
                keep_fields = auto_fields(resource)
                if 1 in client_projection.values():
                    # inclusive projection - all values are 0 unless spec. or
                    # auto
                    fields = dict([(field, field in keep_fields)
                                   for field in fields.keys()])
                for field, value in client_projection.items():
                    field_base = field.split('.')[0]
                    if field_base not in keep_fields and field_base in fields:
                        fields[field] = value
            else:
                # there's no standard projection so we assume we are in a
                # allow_unknown = True
                fields = client_projection
        # always drop exclusion projection, thus avoid mixed projection not
        # supported by db driver
        fields = dict([(field, 1) for field, value in fields.items() if value])

        # If the current HTTP method is in `public_methods` or
        # `public_item_methods`, skip the `auth_field` check

        # Only inject the auth_field in the query when not creating new
        # documents.
        if request and request.method != 'POST' and (
                check_auth_value or force_auth_field_projection):
            auth_field, request_auth_value = auth_field_and_value(resource)
            if auth_field:
                if request_auth_value and check_auth_value:
                    if query:
                        # If the auth_field *replaces* a field in the query,
                        # and the values are /different/, deny the request
                        # This prevents the auth_field condition from
                        # overwriting the query (issue #77)
                        auth_field_in_query = \
                            self.app.data.query_contains_field(query,
                                                               auth_field)
                        if auth_field_in_query and \
                            self.app.data.get_value_from_query(
                                query, auth_field) != request_auth_value:
                            desc = 'Incompatible User-Restricted Resource ' \
                                   'request.'
                            abort(401, description=desc)
                        else:
                            query = self.app.data.combine_queries(
                                query, {auth_field: request_auth_value})
                    else:
                        query = {auth_field: request_auth_value}
                if force_auth_field_projection:
                    fields[auth_field] = 1
        return datasource, query, fields, sort
Exemple #16
0
def put_internal(resource,
                 payload=None,
                 concurrency_check=False,
                 skip_validation=False,
                 **lookup):
    """ Intended for internal put calls, this method is not rate limited,
    authentication is not checked, pre-request events are not raised, and
    concurrency checking is optional. Performs a document replacement.
    Updates are first validated against the resource schema. If validation
    passes, the document is replaced and an OK status update is returned.
    If validation fails a set of validation issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param payload: alternative payload. When calling put() from your own code
                    you can provide an alternative payload. This can be useful,
                    for example, when you have a callback function hooked to a
                    certain endpoint, and want to perform additional put()
                    callsfrom there.

                    Please be advised that in order to successfully use this
                    option, a request context must be available.
    :param concurrency_check: concurrency check switch (bool)
    :param skip_validation: skip payload validation before write (bool)
    :param **lookup: document lookup query.

    .. versionchanged:: 0.6
       Create document if it does not exist. Closes #634.
       Allow restoring soft deleted documents via PUT

    .. versionchanged:: 0.5
       Back to resolving default values after validation as now the validator
       can properly validate dependency even when some have default values. See
       #353.
       Original put() has been split into put() and put_internal().
       You can now pass a pre-defined custom payload to the funcion.
       ETAG is now stored with the document (#369).
       Catching all HTTPExceptions and returning them to the caller, allowing
       for eventual flask.abort() invocations in callback functions to go
       through. Fixes #395.

    .. versionchanged:: 0.4
       Allow abort() to be invoked by callback functions.
       Resolve default values before validation is performed. See #353.
       Raise 'on_replace' instead of 'on_insert'. The callback function gets
       the document (as opposed to a list of just 1 document) as an argument.
       Support for document versioning.
       Raise `on_replaced` after the document has been replaced

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise pre_<method> event.
       explicitly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.
       Item-identifier wrapper stripped from both request and response payload.

    .. versionadded:: 0.1.0
    """
    resource_def = app.config['DOMAIN'][resource]
    schema = resource_def['schema']
    validator = None if skip_validation else app.validator(schema,
                                                           resource=resource)

    if payload is None:
        payload = payload_()

    #### hack ###
    if '_id' in lookup:
        if 'schema' in resource_def and '_id' in resource_def['schema']:
            if resource_def['schema']['_id'].get('type') == 'uuid':
                lookup['_id'] = UUID(lookup['_id'])
    #### end of hack ###

    # Retrieve the original document without checking user-restricted access,
    # but returning the document owner in the projection. This allows us to
    # prevent PUT if the document exists, but is owned by a different user
    # than the currently authenticated one.
    original = get_document(resource,
                            concurrency_check,
                            check_auth_value=False,
                            force_auth_field_projection=True,
                            **lookup)
    if not original:
        if config.UPSERT_ON_PUT:
            id = lookup[resource_def['id_field']]
            # this guard avoids a bson dependency, which would be needed if we
            # wanted to use 'isinstance'. Should also be slightly faster.
            if schema[resource_def['id_field']].get('type', '') == 'objectid':
                id = str(id)
            payload[resource_def['id_field']] = id
            return post_internal(resource,
                                 payl=payload,
                                 skip_validation=skip_validation)
        else:
            abort(404)

    # If the document exists, but is owned by someone else, return
    # 403 Forbidden
    auth_field, request_auth_value = auth_field_and_value(resource)
    if auth_field and original.get(auth_field) != request_auth_value:
        abort(403)

    last_modified = None
    etag = None
    issues = {}
    object_id = original[resource_def['id_field']]

    response = {}

    if config.BANDWIDTH_SAVER is True:
        embedded_fields = []
    else:
        req = parse_request(resource)
        embedded_fields = resolve_embedded_fields(resource, req)

    try:
        document = parse(payload, resource)
        resolve_sub_resource_path(document, resource)
        if skip_validation:
            validation = True
        else:
            validation = validator.validate_replace(document, object_id,
                                                    original)
            # Apply coerced values
            document = validator.document

        if validation:
            # sneak in a shadow copy if it wasn't already there
            late_versioning_catch(original, resource)

            # update meta
            last_modified = datetime.utcnow().replace(microsecond=0)
            # document[config.LAST_UPDATED] = last_modified
            # document[config.DATE_CREATED] = original[config.DATE_CREATED]
            if resource_def['soft_delete'] is True:
                # PUT with soft delete enabled should always set the DELETED
                # field to False. We are either carrying through un-deleted
                # status, or restoring a soft deleted document
                document[config.DELETED] = False

            # id_field not in document means it is not being automatically
            # handled (it has been set to a field which exists in the
            # resource schema.
            if resource_def['id_field'] not in document:
                document[resource_def['id_field']] = object_id

            resolve_user_restricted_access(document, resource)
            store_media_files(document, resource, original)
            resolve_document_version(document, resource, 'PUT', original)

            # notify callbacks
            getattr(app, "on_replace")(resource, document, original)
            getattr(app, "on_replace_%s" % resource)(document, original)

            resolve_document_etag(document, resource)

            # write to db
            try:
                app.data.replace(resource, object_id, document, original)
            except app.data.OriginalChangedError:
                if concurrency_check:
                    abort(412,
                          description='Client and server etags don\'t match')

            # update oplog if needed
            oplog_push(resource, document, 'PUT')

            insert_versioning_documents(resource, document)

            # notify callbacks
            getattr(app, "on_replaced")(resource, document, original)
            getattr(app, "on_replaced_%s" % resource)(document, original)

            # build the full response document
            build_response_document(document, resource, embedded_fields,
                                    document)
            response = document
            if config.IF_MATCH:
                etag = response[config.ETAG]
        else:
            issues = validator.errors
    except DocumentError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues['validator exception'] = str(e)
    except exceptions.HTTPException as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        app.logger.exception(e)
        abort(400,
              description=debug_error_message('An exception occurred: %s' % e))

    if len(issues):
        response[config.ISSUES] = issues
        response[config.STATUS] = config.STATUS_ERR
        status = config.VALIDATION_ERROR_STATUS
    else:
        response[config.STATUS] = config.STATUS_OK
        status = 200

    # limit what actually gets sent to minimize bandwidth usage
    response = marshal_write_response(response, resource)

    return response, last_modified, etag, status
Exemple #17
0
def put_internal(
    resource, payload=None, concurrency_check=False, skip_validation=False, **lookup
):
    """ Intended for internal put calls, this method is not rate limited,
    authentication is not checked, pre-request events are not raised, and
    concurrency checking is optional. Performs a document replacement.
    Updates are first validated against the resource schema. If validation
    passes, the document is replaced and an OK status update is returned.
    If validation fails a set of validation issues is returned.

    :param resource: the name of the resource to which the document belongs.
    :param payload: alternative payload. When calling put() from your own code
                    you can provide an alternative payload. This can be useful,
                    for example, when you have a callback function hooked to a
                    certain endpoint, and want to perform additional put()
                    callsfrom there.

                    Please be advised that in order to successfully use this
                    option, a request context must be available.
    :param concurrency_check: concurrency check switch (bool)
    :param skip_validation: skip payload validation before write (bool)
    :param **lookup: document lookup query.

    .. versionchanged:: 0.6
       Create document if it does not exist. Closes #634.
       Allow restoring soft deleted documents via PUT

    .. versionchanged:: 0.5
       Back to resolving default values after validation as now the validator
       can properly validate dependency even when some have default values. See
       #353.
       Original put() has been split into put() and put_internal().
       You can now pass a pre-defined custom payload to the funcion.
       ETAG is now stored with the document (#369).
       Catching all HTTPExceptions and returning them to the caller, allowing
       for eventual flask.abort() invocations in callback functions to go
       through. Fixes #395.

    .. versionchanged:: 0.4
       Allow abort() to be invoked by callback functions.
       Resolve default values before validation is performed. See #353.
       Raise 'on_replace' instead of 'on_insert'. The callback function gets
       the document (as opposed to a list of just 1 document) as an argument.
       Support for document versioning.
       Raise `on_replaced` after the document has been replaced

    .. versionchanged:: 0.3
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise pre_<method> event.
       explicitly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.
       Item-identifier wrapper stripped from both request and response payload.

    .. versionadded:: 0.1.0
    """
    resource_def = app.config["DOMAIN"][resource]
    schema = resource_def["schema"]
    validator = app.validator(
        schema, resource=resource, allow_unknown=resource_def["allow_unknown"]
    )

    if payload is None:
        payload = payload_()

    # Retrieve the original document without checking user-restricted access,
    # but returning the document owner in the projection. This allows us to
    # prevent PUT if the document exists, but is owned by a different user
    # than the currently authenticated one.
    original = get_document(
        resource,
        concurrency_check,
        check_auth_value=False,
        force_auth_field_projection=True,
        **lookup
    )
    if not original:
        if config.UPSERT_ON_PUT:
            id = lookup[resource_def["id_field"]]
            # this guard avoids a bson dependency, which would be needed if we
            # wanted to use 'isinstance'. Should also be slightly faster.
            if schema[resource_def["id_field"]].get("type", "") == "objectid":
                id = str(id)
            payload[resource_def["id_field"]] = id
            return post_internal(resource, payl=payload)
        else:
            abort(404)

    # If the document exists, but is owned by someone else, return
    # 403 Forbidden
    auth_field, request_auth_value = auth_field_and_value(resource)
    if auth_field and original.get(auth_field) != request_auth_value:
        abort(403)

    last_modified = None
    etag = None
    issues = {}
    object_id = original[resource_def["id_field"]]

    response = {}

    if config.BANDWIDTH_SAVER is True:
        embedded_fields = []
    else:
        req = parse_request(resource)
        embedded_fields = resolve_embedded_fields(resource, req)

    try:
        document = parse(payload, resource)
        resolve_sub_resource_path(document, resource)
        if skip_validation:
            validation = True
        else:
            validation = validator.validate_replace(document, object_id, original)
            # Apply coerced values
            document = validator.document

        if validation:
            # sneak in a shadow copy if it wasn't already there
            late_versioning_catch(original, resource)

            # update meta
            last_modified = datetime.utcnow().replace(microsecond=0)
            document[config.LAST_UPDATED] = last_modified
            document[config.DATE_CREATED] = original[config.DATE_CREATED]
            if resource_def["soft_delete"] is True:
                # PUT with soft delete enabled should always set the DELETED
                # field to False. We are either carrying through un-deleted
                # status, or restoring a soft deleted document
                document[config.DELETED] = False

            # id_field not in document means it is not being automatically
            # handled (it has been set to a field which exists in the
            # resource schema.
            if resource_def["id_field"] not in document:
                document[resource_def["id_field"]] = object_id

            resolve_user_restricted_access(document, resource)
            store_media_files(document, resource, original)
            resolve_document_version(document, resource, "PUT", original)

            # notify callbacks
            getattr(app, "on_replace")(resource, document, original)
            getattr(app, "on_replace_%s" % resource)(document, original)

            resolve_document_etag(document, resource)

            # write to db
            try:
                app.data.replace(resource, object_id, document, original)
            except app.data.OriginalChangedError:
                if concurrency_check:
                    abort(412, description="Client and server etags don't match")

            # update oplog if needed
            oplog_push(resource, document, "PUT")

            insert_versioning_documents(resource, document)

            # notify callbacks
            getattr(app, "on_replaced")(resource, document, original)
            getattr(app, "on_replaced_%s" % resource)(document, original)

            # build the full response document
            build_response_document(document, resource, embedded_fields, document)
            response = document
            if config.IF_MATCH:
                etag = response[config.ETAG]
        else:
            issues = validator.errors
    except DocumentError as e:
        # TODO should probably log the error and abort 400 instead (when we
        # got logging)
        issues["validator exception"] = str(e)
    except exceptions.HTTPException as e:
        raise e
    except Exception as e:
        # consider all other exceptions as Bad Requests
        app.logger.exception(e)
        abort(400, description=debug_error_message("An exception occurred: %s" % e))

    if len(issues):
        response[config.ISSUES] = issues
        response[config.STATUS] = config.STATUS_ERR
        status = config.VALIDATION_ERROR_STATUS
    else:
        response[config.STATUS] = config.STATUS_OK
        status = 200

    # limit what actually gets sent to minimize bandwidth usage
    response = marshal_write_response(response, resource)

    return response, last_modified, etag, status