Exemple #1
0
def get_namespace_by_name_from_users_namespaces(user,
                                                namespace_name,
                                                raise_exception=False):
    """check and get namespace from users namespaces by name

    :param ..models.res_users.ResUsers user: The user record.
    :param str namespace_name: The name of namespace.
    :param bool raise_exception: raise exception if namespace does not exist.

    :returns: Found 'openapi.namespace' record.
    :rtype: ..models.openapi_namespace.Namespace

    :raise: werkzeug.exceptions.HTTPException if the namespace is not contained
                                              in allowed user namespaces.
    """
    namespace = request.env["openapi.namespace"].search([("name", "=",
                                                          namespace_name)])

    if not namespace.exists() and raise_exception:
        raise werkzeug.exceptions.HTTPException(response=error_response(
            *CODE__obj_not_found))

    if namespace not in user.namespace_ids and raise_exception:
        err = list(CODE__user_no_perm)
        err[2] = "The requested namespace (integration) is not authorized."
        raise werkzeug.exceptions.HTTPException(response=error_response(*err))

    return namespace
Exemple #2
0
def get_data_from_auth_header(header):
    """decode basic auth header and get data

    :param str header: The raw auth header.

    :returns: a tuple of database name and user token
    :rtype: tuple
    :raise: werkzeug.exceptions.HTTPException if basic header is invalid base64
                                              string or if the basic header is
                                              in the wrong format
    """
    normalized_token = header.replace("Basic ", "").replace("\\n",
                                                            "").encode("utf-8")
    try:
        decoded_token_parts = (
            base64.b64decode(normalized_token).decode("utf-8").split(":"))
    except TypeError:
        raise werkzeug.exceptions.HTTPException(response=error_response(
            500, "Invalid header",
            "Basic auth header must be valid base64 string"))

    if len(decoded_token_parts) == 1:
        db_name, user_token = None, decoded_token_parts[0]
    elif len(decoded_token_parts) == 2:
        db_name, user_token = decoded_token_parts
    else:
        err_descrip = (
            'Basic auth header payload must be of the form "<%s>" (encoded to base64)'
            % "user_token"
            if odoo.tools.config["dbfilter"] else "db_name:user_token")
        raise werkzeug.exceptions.HTTPException(
            response=error_response(500, "Invalid header", err_descrip))

    return db_name, user_token
Exemple #3
0
def get_dict_from_model(model, spec, id, **kwargs):
    """Fetch dictionary from one record according to spec.

    :param str model: The model against which to validate.
    :param tuple spec: The spec to validate.
    :param int id: The id of the record.
    :param dict kwargs: Keyword arguments.
    :param tuple kwargs['include_fields']: The extra fields.
        This parameter is not implemented on higher level code in order
        to serve as a soft ACL implementation on top of the framework's
        own ACL.
    :param tuple kwargs['exclude_fields']: The excluded fields.

    :returns: The python dictionary of the requested values.
    :rtype: dict
    :raise: werkzeug.exceptions.HTTPException if the record does not exist.
    """
    include_fields = kwargs.get(
        "include_fields",
        ())  # Not actually implemented on higher level (ACL!)
    exclude_fields = kwargs.get("exclude_fields", ())

    model_obj = get_model_for_read(model)

    record = model_obj.browse([id])
    if not record.exists():
        raise werkzeug.exceptions.HTTPException(response=error_response(
            *CODE__res_not_found))
    return get_dict_from_record(record, spec, include_fields, exclude_fields)
Exemple #4
0
def wrap__resource__call_method(modelname, ids, method, method_params,
                                success_code):
    """Function to call the model method for records by IDs.

    :param str modelname: The name of the model.
    :param list ids: The record ids of which we want to call method.
    :param str method: The name of the method.
    :param int success_code: The success code.

    :returns: successful response if the method execution did not cause an error
              otherwise error response
    :rtype: werkzeug.wrappers.Response
    """
    model_obj = get_model_for_read(modelname)

    if not hasattr(model_obj, method):
        return error_response(*CODE__invalid_method)

    records = model_obj.browse(ids).exists()
    results = []
    args = method_params.get("args", [])
    kwargs = method_params.get("kwargs", {})
    for record in records or [model_obj]:
        result = getattr(record, method)(*args, **kwargs)
        results.append(result)

    if len(ids) <= 1 and len(results):
        results = results[0]
    model_obj.flush()  # to recompute fields
    return successful_response(success_code, data=results)
Exemple #5
0
def wrap__resource__create_one(modelname, context, data, success_code,
                               out_fields):
    """Function to create one record.

    :param str model: The name of the model.
    :param dict context: TODO
    :param dict data: Data received from the user.
    :param int success_code: The success code.
    :param tuple out_fields: Canned fields.

    :returns: successful response if the create operation is performed
              otherwise error response
    :rtype: werkzeug.wrappers.Response
    """
    model_obj = get_model_for_read(modelname)
    try:
        created_obj = model_obj.with_context(context).create(data)
        test_mode = request.registry.test_cr
        if not test_mode:
            # Somehow don't making a commit here may lead to error
            # "Record does not exist or has been deleted"
            # Probably, Odoo (10.0 at least) uses different cursors
            # to create and to read fields from database
            request.env.cr.commit()
    except Exception as e:
        return error_response(400, type(e).__name__, str(e))

    out_data = get_dict_from_record(created_obj, out_fields, (), ())
    return successful_response(success_code, out_data)
Exemple #6
0
def authenticate_token_for_user(token):
    """Authenticate against the database and setup user session corresponding to the token.

    :param str token: The raw access token.

    :returns: User if token is authorized for the requested user.
    :rtype odoo.models.Model

    :raise: werkzeug.exceptions.HTTPException if user not found.
    """
    user = request.env["res.users"].sudo().search([("openapi_token", "=",
                                                    token)])
    if user.exists():
        # copy-pasted from odoo.http.py:OpenERPSession.authenticate()
        request.session.uid = user.id
        request.session.login = user.login
        request.session.session_token = user.id and security.compute_session_token(
            request.session, request.env)
        request.uid = user.id
        request.disable_db = False
        request.session.get_context()

        return user
    raise werkzeug.exceptions.HTTPException(response=error_response(
        *CODE__no_user_auth))
Exemple #7
0
def wrap__resource__update_one(modelname, id, success_code, data):
    """Function to update one record.

    :param str modelname: The name of the model.
    :param int id: The record id of which we want to update.
    :param int success_code: The success code.
    :param dict data: The data for update.

    :returns: successful response if the update operation is performed
              otherwise error response
    :rtype: werkzeug.wrappers.Response
    """
    cr, uid = request.cr, request.session.uid
    record = request.env(cr, uid)[modelname].browse(id)
    if not record.exists():
        return error_response(*CODE__obj_not_found)
    try:
        record.write(data)
    except Exception as e:
        return error_response(400, type(e).__name__, str(e))
    return successful_response(success_code)
Exemple #8
0
def setup_db(httprequest, db_name):
    """check and setup db in session by db name

    :param httprequest: a wrapped werkzeug Request object
    :type httprequest: :class:`werkzeug.wrappers.BaseRequest`
    :param str db_name: Database name.

    :raise: werkzeug.exceptions.HTTPException if the database not found.
    """
    if httprequest.session.db:
        return
    if db_name not in odoo.service.db.list_dbs(force=True):
        raise werkzeug.exceptions.HTTPException(response=error_response(
            *CODE__db_not_found))

    httprequest.session.db = db_name
Exemple #9
0
def wrap__resource__unlink_one(modelname, id, success_code):
    """Function to delete one record.

    :param str modelname: The name of the model.
    :param int id: The record id of which we want to delete.
    :param int success_code: The success code.

    :returns: successful response if the delete operation is performed
              otherwise error response
    :rtype: werkzeug.wrappers.Response
    """
    cr, uid = request.cr, request.session.uid
    record = request.env(cr, uid)[modelname].browse([id])
    if not record.exists():
        return error_response(*CODE__obj_not_found)
    record.unlink()
    return successful_response(success_code)
Exemple #10
0
def method_is_allowed(method, methods_conf, main=False, raise_exception=False):
    """Check that the method is allowed for the specified settings.

    :param str method: The name of the method.
    :param dict methods_conf: The methods configuration dictionary.
        A dictionary containing the methods API configuration.
            The layout of the dict is as follows:
            ```python
            {
                'public' : {
                     'mode':            (String)    one of 'all', 'none', 'custom',
                     'whitelist':       (List)      of method strings,
                 },
                'private' : {
                     'mode':            (String)    one of 'none', 'custom',
                     'whitelist':       (List)      of method strings,
                 },
                'main' : {
                     'mode':            (String)    one of 'none', 'custom',
                     'whitelist':       (List)      of method strings,
                 },
            }
            ```
    :param bool main: The method is a one of access fields.
    :param bool raise_exception: raise exception instead of returning **False**.

    :returns: **True** if the method is allowed, otherwise **False**.
    :rtype: bool
    :raise: werkzeug.exceptions.HTTPException if the method is not allowed and
                                              raise_exception is **True**.
    """
    if main:
        method_type = "main"
    else:
        method_type = "private" if method.startswith("_") else "public"

    if methods_conf[method_type]["mode"] == "all":
        return True
    if (methods_conf[method_type]["mode"] == "custom"
            and method in methods_conf[method_type]["whitelist"]):
        return True
    if raise_exception:
        raise werkzeug.exceptions.HTTPException(response=error_response(
            *CODE__method_blocked))
    return False
Exemple #11
0
def get_auth_header(headers, raise_exception=False):
    """check and get basic authentication header from headers

    :param werkzeug.datastructures.Headers headers: All headers in request.
    :param bool raise_exception: raise exception.

    :returns: Found raw authentication header.
    :rtype: str or None

    :raise: werkzeug.exceptions.HTTPException if raise_exception is **True**
                                              and auth header is not in headers
                                              or it is not Basic type.
    """
    auth_header = headers.get("Authorization") or headers.get("authorization")
    if not auth_header or not auth_header.startswith("Basic "):
        if raise_exception:
            raise werkzeug.exceptions.HTTPException(response=error_response(
                *CODE__no_user_auth))
    return auth_header
Exemple #12
0
        def controller_method_wrapper(*iargs, **ikwargs):

            auth_header = get_auth_header(request.httprequest.headers,
                                          raise_exception=True)
            db_name, user_token = get_data_from_auth_header(auth_header)
            setup_db(request.httprequest, db_name)
            authenticated_user = authenticate_token_for_user(user_token)
            namespace = get_namespace_by_name_from_users_namespaces(
                authenticated_user, ikwargs["namespace"], raise_exception=True)
            data_for_log = {
                "namespace_id": namespace.id,
                "namespace_log_request": namespace.log_request,
                "namespace_log_response": namespace.log_response,
                "user_id": authenticated_user.id,
                "user_request": None,
                "user_response": None,
            }

            try:
                response = controller_method(*iargs, **ikwargs)
            except werkzeug.exceptions.HTTPException as e:
                response = e.response
            except Exception as e:
                traceback.print_exc()
                if hasattr(e, "error") and isinstance(e.error, Exception):
                    e = e.error
                response = error_response(
                    status=500,
                    error=type(e).__name__,
                    error_descrip=e.name if hasattr(e, "name") else str(e),
                )

            data_for_log.update({
                "user_request": request.httprequest,
                "user_response": response
            })
            create_log_record(**data_for_log)

            return response
Exemple #13
0
def get_create_context(namespace, model, canned_context):
    """Get the requested preconfigured context of the model specification.

    The canned context is used to preconfigure default values or context flags.
    That are used in a repetitive way in namespace for specific model.

    As this should, for performance reasons, not repeatedly result in calls to the persistence
    layer, this method is cached in memory.

    :param str namespace: The namespace to also validate against.
    :param str model: The model, for which we retrieve the configuration.
    :param str canned_context: The preconfigured context, which we request.

    :returns: A dictionary containing the requested context.
    :rtype: dict
    :raise: werkzeug.exceptions.HTTPException TODO: add description in which case
    """
    cr, uid = request.cr, request.session.uid

    # Singleton by construction (_sql_constraints)
    openapi_access = request.env(cr, uid)["openapi.access"].search([
        ("model_id", "=", model), ("namespace_id.name", "=", namespace)
    ])

    assert (len(openapi_access) == 1
            ), "'openapi_access' is not a singleton, bad construction."
    # Singleton by construction (_sql_constraints)
    context = openapi_access.create_context_ids.filtered(
        lambda r: r["name"] == canned_context)
    assert len(context) == 1, "'context' is not a singleton, bad construction."

    if not context:
        raise werkzeug.exceptions.HTTPException(response=error_response(
            *CODE__canned_ctx_not_found))

    return context
Exemple #14
0
def get_model_openapi_access(namespace, model):
    """Get the model configuration and validate the requested namespace against the session.

    The namespace is a lightweight ACL + default implementation to integrate
    with various integration consumer, such as webstore, provisioning platform, etc.

    We validate the namespace at this latter stage, because it forms part of the http route.
    The token has been related to a namespace already previously

    This is a double purpose method.

    As this should, for performance reasons, not repeatedly result in calls to the persistence
    layer, this method is cached in memory.

    :param str namespace: The namespace to also validate against.
    :param str model: The model, for which we retrieve the configuration.

    :returns: The error response object if namespace validation failed.
        A dictionary containing the model API configuration for this namespace.
            The layout of the dict is as follows:
            ```python
            {'context':                 (Dict)      odoo context (default values through context),
            'out_fields_read_multi':    (Tuple)     field spec,
            'out_fields_read_one':      (Tuple)     field spec,
            'out_fields_create_one':    (Tuple)     field spec,
            'method' : {
                'public' : {
                     'mode':            (String)    one of 'all', 'none', 'custom',
                     'whitelist':       (List)      of method strings,
                 },
                'private' : {
                     'mode':            (String)    one of 'none', 'custom',
                     'whitelist':       (List)      of method strings,
                 },
                'main' : {
                     'mode':            (String)    one of 'none', 'custom',
                     'whitelist':       (List)      of method strings,
                 },
            }
            ```
    :rtype: dict
    :raise: werkzeug.exceptions.HTTPException if the namespace has no accesses.
    """
    # TODO: this method has code duplicates with openapi specification code (e.g. get_OAS_definitions_part)
    cr, uid = request.cr, request.session.uid
    # Singleton by construction (_sql_constraints)
    openapi_access = (request.env(cr, uid)["openapi.access"].sudo().search([
        ("model_id", "=", model), ("namespace_id.name", "=", namespace)
    ]))
    if not openapi_access.exists():
        raise werkzeug.exceptions.HTTPException(response=error_response(
            *CODE__canned_ctx_not_found))

    res = {
        "context":
        {},  # Take ot here FIXME: make sure it is for create_context
        "out_fields_read_multi": (),
        "out_fields_read_one": (),
        "out_fields_create_one": (),  # FIXME: for what?
        "method": {
            "public": {
                "mode": "",
                "whitelist": []
            },
            "private": {
                "mode": "",
                "whitelist": []
            },
            "main": {
                "mode": "",
                "whitelist": []
            },
        },
    }
    # Infer public method mode
    if openapi_access.api_public_methods and openapi_access.public_methods:
        res["method"]["public"]["mode"] = "custom"
        res["method"]["public"][
            "whitelist"] = openapi_access.public_methods.split()
    elif openapi_access.api_public_methods:
        res["method"]["public"]["mode"] = "all"
    else:
        res["method"]["public"]["mode"] = "none"

    # Infer private method mode
    if openapi_access.private_methods:
        res["method"]["private"]["mode"] = "custom"
        res["method"]["private"][
            "whitelist"] = openapi_access.private_methods.split()
    else:
        res["method"]["private"]["mode"] = "none"

    for c in openapi_access.create_context_ids.mapped("context"):
        res["context"].update(json.loads(c))

    res["out_fields_read_multi"] = openapi_access.read_many_id.export_fields.mapped(
        "name") or ("id", )
    res["out_fields_read_one"] = openapi_access.read_one_id.export_fields.mapped(
        "name") or ("id", )

    if openapi_access.public_methods:
        res["method"]["public"][
            "whitelist"] = openapi_access.public_methods.split()
    if openapi_access.private_methods:
        res["method"]["private"][
            "whitelist"] = openapi_access.private_methods.split()

    main_methods = ["api_create", "api_read", "api_update", "api_delete"]
    for method in main_methods:
        if openapi_access[method]:
            res["method"]["main"]["whitelist"].append(method)

    if len(res["method"]["main"]["whitelist"]) == len(main_methods):
        res["method"]["main"]["mode"] = "all"
    elif not res["method"]["main"]["whitelist"]:
        res["method"]["main"]["mode"] = "none"
    else:
        res["method"]["main"]["mode"] = "custom"

    return res