Passed
Push — master ( b4f401...16d7f4 )
by
unknown
11:43 queued 14s
created

EvaluationPlaneDisabledHandler.initialize()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nop 3
crap 1
1 1
from tabpy.tabpy_server.handlers import BaseHandler
2 1
import json
3 1
import simplejson
4 1
import logging
5 1
from tabpy.tabpy_server.common.util import format_exception
6 1
import requests
7 1
from tornado import gen
8 1
from datetime import timedelta
9 1
from tabpy.tabpy_server.handlers.util import AuthErrorStates
10
11
12 1
class RestrictedTabPy:
13 1
    def __init__(self, protocol, port, logger, timeout, headers):
14 1
        self.protocol = protocol
15 1
        self.port = port
16 1
        self.logger = logger
17 1
        self.timeout = timeout
18 1
        self.headers = headers
19
20 1
    def query(self, name, *args, **kwargs):
21
        url = f"{self.protocol}://localhost:{self.port}/query/{name}"
22
        self.logger.log(logging.DEBUG, f"Querying {url}...")
23
        internal_data = {"data": args or kwargs}
24
        data = json.dumps(internal_data)
25
        headers = self.headers
26
        response = requests.post(
27
            url=url, data=data, headers=headers, timeout=self.timeout, verify=False
28
        )
29
        return response.json()
30
31
32 1
class EvaluationPlaneDisabledHandler(BaseHandler):
33
    """
34
    EvaluationPlaneDisabledHandler responds with error message when ad-hoc scripts have been disabled.
35
    """
36
37 1
    def initialize(self, executor, app):
38 1
        super(EvaluationPlaneDisabledHandler, self).initialize(app)
39 1
        self.executor = executor
40
41 1
    @gen.coroutine
42
    def post(self):
43 1
        if self.should_fail_with_auth_error() != AuthErrorStates.NONE:
44 1
            self.fail_with_auth_error()
45 1
            return
46 1
        self.error_out(404, "Ad-hoc scripts have been disabled on this analytics extension, please contact your "
47
                            "administrator.")
48
49
50 1
class EvaluationPlaneHandler(BaseHandler):
51
    """
52
    EvaluationPlaneHandler is responsible for running arbitrary python scripts.
53
    """
54
55 1
    def initialize(self, executor, app):
56 1
        super(EvaluationPlaneHandler, self).initialize(app)
57 1
        self.executor = executor
58 1
        self._error_message_timeout = (
59
            f"User defined script timed out. "
60
            f"Timeout is set to {self.eval_timeout} s."
61
        )
62
63 1
    @gen.coroutine
64
    def _post_impl(self):
65 1
        body = json.loads(self.request.body.decode("utf-8"))
66 1
        self.logger.log(logging.DEBUG, f"Processing POST request '{body}'...")
67 1
        if "script" not in body:
68 1
            self.error_out(400, "Script is empty.")
69 1
            return
70
71
        # Transforming user script into a proper function.
72 1
        user_code = body["script"]
73 1
        arguments = None
74 1
        arguments_str = ""
75 1
        if "data" in body:
76 1
            arguments = body["data"]
77
78 1
        if arguments is not None:
79 1
            if not isinstance(arguments, dict):
80
                self.error_out(
81
                    400, "Script parameters need to be provided as a dictionary."
82
                )
83
                return
84 1
            args_in = sorted(arguments.keys())
85 1
            n = len(arguments)
86 1
            if sorted('_arg'+str(i+1) for i in range(n)) == args_in:
87 1
                arguments_str = ", " + ", ".join(args_in)
88
            else:
89 1
                self.error_out(
90
                    400,
91
                    "Variables names should follow "
92
                    "the format _arg1, _arg2, _argN",
93
                )
94 1
                return
95 1
        function_to_evaluate = f"def _user_script(tabpy{arguments_str}):\n"
96 1
        for u in user_code.splitlines():
97 1
            function_to_evaluate += " " + u + "\n"
98
99 1
        self.logger.log(
100
            logging.INFO, f"function to evaluate={function_to_evaluate}"
101
        )
102
103 1
        try:
104 1
            result = yield self._call_subprocess(function_to_evaluate, arguments)
105 1
        except (
106
            gen.TimeoutError,
107
            requests.exceptions.ConnectTimeout,
108
            requests.exceptions.ReadTimeout,
109
        ):
110
            self.logger.log(logging.ERROR, self._error_message_timeout)
111
            self.error_out(408, self._error_message_timeout)
112
            return
113
114 1
        if result is not None:
115 1
            self.write(simplejson.dumps(result, ignore_nan=True))
116
        else:
117 1
            self.write("null")
118 1
        self.finish()
119
120 1
    @gen.coroutine
121
    def post(self):
122 1
        if self.should_fail_with_auth_error() != AuthErrorStates.NONE:
123 1
            self.fail_with_auth_error()
124 1
            return
125
126 1
        self._add_CORS_header()
127 1
        try:
128 1
            yield self._post_impl()
129 1
        except Exception as e:
130 1
            err_msg = f"{e.__class__.__name__} : {str(e)}"
131 1
            if err_msg != "KeyError : 'response'":
132 1
                err_msg = format_exception(e, "POST /evaluate")
133 1
                self.error_out(500, "Error processing script", info=err_msg)
134
            else:
135
                self.error_out(
136
                    404,
137
                    "Error processing script",
138
                    info="The endpoint you're "
139
                    "trying to query did not respond. Please make sure the "
140
                    "endpoint exists and the correct set of arguments are "
141
                    "provided.",
142
                )
143
144 1
    @gen.coroutine
145
    def _call_subprocess(self, function_to_evaluate, arguments):
146 1
        restricted_tabpy = RestrictedTabPy(
147
            self.protocol, self.port, self.logger, self.eval_timeout, self.request.headers
148
        )
149
        # Exec does not run the function, so it does not block.
150 1
        exec(function_to_evaluate, globals())
151
152
        # 'noqa' comments below tell flake8 to ignore undefined _user_script
153
        # name - the name is actually defined with user script being wrapped
154
        # in _user_script function (constructed as a striong) and then executed
155
        # with exec() call above.
156 1
        future = self.executor.submit(_user_script,  # noqa: F821
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable _user_script does not seem to be defined.
Loading history...
157
                                      restricted_tabpy,
158
                                      **arguments if arguments is not None else None)
159
160 1
        ret = yield gen.with_timeout(timedelta(seconds=self.eval_timeout), future)
161
        raise gen.Return(ret)
162