We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
The API key for one query may be used to retrieve another query's results
### Issue Summary

A query's API key may be used to obtain another query's results via the REST api when including the API key in the query string.

### Steps to Reproduce

1. Create one query and execute it to obtain results (call it query A)
2. Create another query and execute it to obtain different results (call it query B)
3. Get the query's API key for query A (A_API_KEY) and the query number for query A (A_QUERY_NUMBER)
4. Get the result number from query B's most recent run (B_RESULT_NUMBER)
5. Execute the below code and you'll see that the API key for query A can get results from query B

```bash
REDASH_DOMAIN='yourdomain.com'
A_QUERY_NUMBER='query number for query A'
A_API_KEY_A='api key for query A'
B_RESULT_NUMBER='query result number for query b'

# this will download query B's results using query A's access key
wget \
  -O query_b_results.csv \
  "https://$REDASH_DOMAIN/api/queries/$A_QUERY_NUMBER/results/$B_RESULT_NUMBER.csv?api_key=$A_API_KEY"
```

This is a bug because one query's API key should NOT be able to access another query's results.

### Technical details:

* Redash Version: 1.0.3
* Browser/OS: (Command Line) / Linux Mint 18.2
* How did you install Redash: Command line


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `redash/handlers/query_results.py`
Content:
```
1 import logging
2 import json
3 import time
4 
5 import pystache
6 from flask import make_response, request
7 from flask_login import current_user
8 from flask_restful import abort
9 from redash import models, settings, utils
10 from redash.tasks import QueryTask, record_event
11 from redash.permissions import require_permission, not_view_only, has_access, require_access, view_only
12 from redash.handlers.base import BaseResource, get_object_or_404
13 from redash.utils import collect_query_parameters, collect_parameters_from_request, gen_query_hash
14 from redash.tasks.queries import enqueue_query
15 
16 
17 def error_response(message):
18     return {'job': {'status': 4, 'error': message}}, 400
19 
20 
21 #
22 # Run a parameterized query synchronously and return the result
23 # DISCLAIMER: Temporary solution to support parameters in queries. Should be
24 #             removed once we refactor the query results API endpoints and handling
25 #             on the client side. Please don't reuse in other API handlers.
26 #
27 def run_query_sync(data_source, parameter_values, query_text, max_age=0):
28     query_parameters = set(collect_query_parameters(query_text))
29     missing_params = set(query_parameters) - set(parameter_values.keys())
30     if missing_params:
31         raise Exception('Missing parameter value for: {}'.format(", ".join(missing_params)))
32 
33     if query_parameters:
34         query_text = pystache.render(query_text, parameter_values)
35 
36     if max_age <= 0:
37         query_result = None
38     else:
39         query_result = models.QueryResult.get_latest(data_source, query_text, max_age)
40 
41     query_hash = gen_query_hash(query_text)
42 
43     if query_result:
44         logging.info("Returning cached result for query %s" % query_hash)
45         return query_result
46 
47     try:
48         started_at = time.time()
49         data, error = data_source.query_runner.run_query(query_text, current_user)
50 
51         if error:
52             logging.info('got bak error')
53             logging.info(error)
54             return None
55 
56         run_time = time.time() - started_at
57         query_result, updated_query_ids = models.QueryResult.store_result(data_source.org, data_source,
58                                                                               query_hash, query_text, data,
59                                                                               run_time, utils.utcnow())
60 
61         models.db.session.commit()
62         return query_result
63     except Exception as e:
64         if max_age > 0:
65             abort(404, message="Unable to get result from the database, and no cached query result found.")
66         else:
67             abort(503, message="Unable to get result from the database.")
68         return None
69 
70 def run_query(data_source, parameter_values, query_text, query_id, max_age=0):
71     query_parameters = set(collect_query_parameters(query_text))
72     missing_params = set(query_parameters) - set(parameter_values.keys())
73     if missing_params:
74         return error_response('Missing parameter value for: {}'.format(", ".join(missing_params)))
75 
76     if data_source.paused:
77         if data_source.pause_reason:
78             message = '{} is paused ({}). Please try later.'.format(data_source.name, data_source.pause_reason)
79         else:
80             message = '{} is paused. Please try later.'.format(data_source.name)
81 
82         return error_response(message)
83 
84     if query_parameters:
85         query_text = pystache.render(query_text, parameter_values)
86 
87     if max_age == 0:
88         query_result = None
89     else:
90         query_result = models.QueryResult.get_latest(data_source, query_text, max_age)
91 
92     if query_result:
93         return {'query_result': query_result.to_dict()}
94     else:
95         job = enqueue_query(query_text, data_source, current_user.id, metadata={"Username": current_user.email, "Query ID": query_id})
96         return {'job': job.to_dict()}
97 
98 
99 class QueryResultListResource(BaseResource):
100     @require_permission('execute_query')
101     def post(self):
102         """
103         Execute a query (or retrieve recent results).
104 
105         :qparam string query: The query text to execute
106         :qparam number query_id: The query object to update with the result (optional)
107         :qparam number max_age: If query results less than `max_age` seconds old are available, return them, otherwise execute the query; if omitted, always execute
108         :qparam number data_source_id: ID of data source to query
109         """
110         params = request.get_json(force=True)
111         parameter_values = collect_parameters_from_request(request.args)
112 
113         query = params['query']
114         max_age = int(params.get('max_age', -1))
115         query_id = params.get('query_id', 'adhoc')
116 
117         data_source = models.DataSource.get_by_id_and_org(params.get('data_source_id'), self.current_org)
118 
119         if not has_access(data_source.groups, self.current_user, not_view_only):
120             return {'job': {'status': 4, 'error': 'You do not have permission to run queries with this data source.'}}, 403
121 
122         self.record_event({
123             'action': 'execute_query',
124             'timestamp': int(time.time()),
125             'object_id': data_source.id,
126             'object_type': 'data_source',
127             'query': query
128         })
129         return run_query(data_source, parameter_values, query, query_id, max_age)
130 
131 
132 ONE_YEAR = 60 * 60 * 24 * 365.25
133 
134 
135 class QueryResultResource(BaseResource):
136     @staticmethod
137     def add_cors_headers(headers):
138         if 'Origin' in request.headers:
139             origin = request.headers['Origin']
140 
141             if set(['*', origin]) & settings.ACCESS_CONTROL_ALLOW_ORIGIN:
142                 headers['Access-Control-Allow-Origin'] = origin
143                 headers['Access-Control-Allow-Credentials'] = str(settings.ACCESS_CONTROL_ALLOW_CREDENTIALS).lower()
144 
145     @require_permission('view_query')
146     def options(self, query_id=None, query_result_id=None, filetype='json'):
147         headers = {}
148         self.add_cors_headers(headers)
149 
150         if settings.ACCESS_CONTROL_REQUEST_METHOD:
151             headers['Access-Control-Request-Method'] = settings.ACCESS_CONTROL_REQUEST_METHOD
152 
153         if settings.ACCESS_CONTROL_ALLOW_HEADERS:
154             headers['Access-Control-Allow-Headers'] = settings.ACCESS_CONTROL_ALLOW_HEADERS
155 
156         return make_response("", 200, headers)
157 
158     @require_permission('view_query')
159     def get(self, query_id=None, query_result_id=None, filetype='json'):
160         """
161         Retrieve query results.
162 
163         :param number query_id: The ID of the query whose results should be fetched
164         :param number query_result_id: the ID of the query result to fetch
165         :param string filetype: Format to return. One of 'json', 'xlsx', or 'csv'. Defaults to 'json'.
166 
167         :<json number id: Query result ID
168         :<json string query: Query that produced this result
169         :<json string query_hash: Hash code for query text
170         :<json object data: Query output
171         :<json number data_source_id: ID of data source that produced this result
172         :<json number runtime: Length of execution time in seconds
173         :<json string retrieved_at: Query retrieval date/time, in ISO format
174         """
175         # TODO:
176         # This method handles two cases: retrieving result by id & retrieving result by query id.
177         # They need to be split, as they have different logic (for example, retrieving by query id
178         # should check for query parameters and shouldn't cache the result).
179         should_cache = query_result_id is not None
180 
181         parameter_values = collect_parameters_from_request(request.args)
182         max_age = int(request.args.get('maxAge', 0))
183 
184         query_result = None
185 
186         if query_result_id:
187             query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)
188         elif query_id is not None:
189             query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
190 
191             if query is not None:
192                 if settings.ALLOW_PARAMETERS_IN_EMBEDS and parameter_values:
193                     query_result = run_query_sync(query.data_source, parameter_values, query.to_dict()['query'], max_age=max_age)
194                 elif query.latest_query_data_id is not None:
195                     query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)
196 
197         if query_result:
198             require_access(query_result.data_source.groups, self.current_user, view_only)
199 
200             if isinstance(self.current_user, models.ApiUser):
201                 event = {
202                     'user_id': None,
203                     'org_id': self.current_org.id,
204                     'action': 'api_get',
205                     'timestamp': int(time.time()),
206                     'api_key': self.current_user.name,
207                     'file_type': filetype,
208                     'user_agent': request.user_agent.string,
209                     'ip': request.remote_addr
210                 }
211 
212                 if query_id:
213                     event['object_type'] = 'query'
214                     event['object_id'] = query_id
215                 else:
216                     event['object_type'] = 'query_result'
217                     event['object_id'] = query_result_id
218 
219                 record_event.delay(event)
220 
221             if filetype == 'json':
222                 response = self.make_json_response(query_result)
223             elif filetype == 'xlsx':
224                 response = self.make_excel_response(query_result)
225             else:
226                 response = self.make_csv_response(query_result)
227 
228             if len(settings.ACCESS_CONTROL_ALLOW_ORIGIN) > 0:
229                 self.add_cors_headers(response.headers)
230 
231             if should_cache:
232                 response.headers.add_header('Cache-Control', 'max-age=%d' % ONE_YEAR)
233 
234             return response
235 
236         else:
237             abort(404, message='No cached result found for this query.')
238 
239     def make_json_response(self, query_result):
240         data = json.dumps({'query_result': query_result.to_dict()}, cls=utils.JSONEncoder)
241         headers = {'Content-Type': "application/json"}
242         return make_response(data, 200, headers)
243 
244     @staticmethod
245     def make_csv_response(query_result):
246         headers = {'Content-Type': "text/csv; charset=UTF-8"}
247         return make_response(query_result.make_csv_content(), 200, headers)
248 
249     @staticmethod
250     def make_excel_response(query_result):
251         headers = {'Content-Type': "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}
252         return make_response(query_result.make_excel_content(), 200, headers)
253 
254 
255 class JobResource(BaseResource):
256     def get(self, job_id):
257         """
258         Retrieve info about a running query job.
259         """
260         job = QueryTask(job_id=job_id)
261         return {'job': job.to_dict()}
262 
263     def delete(self, job_id):
264         """
265         Cancel a query job in progress.
266         """
267         job = QueryTask(job_id=job_id)
268         job.cancel()
269
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py
--- a/redash/handlers/query_results.py
+++ b/redash/handlers/query_results.py
@@ -185,14 +185,19 @@
 
         if query_result_id:
             query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)
-        elif query_id is not None:
+
+        if query_id is not None:
             query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
 
-            if query is not None:
+            if query_result is None and query is not None:
                 if settings.ALLOW_PARAMETERS_IN_EMBEDS and parameter_values:
                     query_result = run_query_sync(query.data_source, parameter_values, query.to_dict()['query'], max_age=max_age)
                 elif query.latest_query_data_id is not None:
                     query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)
+                
+            if query is not None and query_result is not None and self.current_user.is_api_user():
+                if query.query_hash != query_result.query_hash:
+                    abort(404, message='No cached result found for this query.')
 
         if query_result:
             require_access(query_result.data_source.groups, self.current_user, view_only)
@@ -229,7 +234,7 @@
                 self.add_cors_headers(response.headers)
 
             if should_cache:
-                response.headers.add_header('Cache-Control', 'max-age=%d' % ONE_YEAR)
+                response.headers.add_header('Cache-Control', 'private,max-age=%d' % ONE_YEAR)
 
             return response

{"golden_diff": "diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py\n--- a/redash/handlers/query_results.py\n+++ b/redash/handlers/query_results.py\n@@ -185,14 +185,19 @@\n \n         if query_result_id:\n             query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)\n-        elif query_id is not None:\n+\n+        if query_id is not None:\n             query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)\n \n-            if query is not None:\n+            if query_result is None and query is not None:\n                 if settings.ALLOW_PARAMETERS_IN_EMBEDS and parameter_values:\n                     query_result = run_query_sync(query.data_source, parameter_values, query.to_dict()['query'], max_age=max_age)\n                 elif query.latest_query_data_id is not None:\n                     query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)\n+                \n+            if query is not None and query_result is not None and self.current_user.is_api_user():\n+                if query.query_hash != query_result.query_hash:\n+                    abort(404, message='No cached result found for this query.')\n \n         if query_result:\n             require_access(query_result.data_source.groups, self.current_user, view_only)\n@@ -229,7 +234,7 @@\n                 self.add_cors_headers(response.headers)\n \n             if should_cache:\n-                response.headers.add_header('Cache-Control', 'max-age=%d' % ONE_YEAR)\n+                response.headers.add_header('Cache-Control', 'private,max-age=%d' % ONE_YEAR)\n \n             return response\n", "issue": "The API key for one query may be used to retrieve another query's results\n### Issue Summary\r\n\r\nA query's API key may be used to obtain another query's results via the REST api when including the API key in the query string.\r\n\r\n### Steps to Reproduce\r\n\r\n1. Create one query and execute it to obtain results (call it query A)\r\n2. Create another query and execute it to obtain different results (call it query B)\r\n3. Get the query's API key for query A (A_API_KEY) and the query number for query A (A_QUERY_NUMBER)\r\n4. Get the result number from query B's most recent run (B_RESULT_NUMBER)\r\n5. Execute the below code and you'll see that the API key for query A can get results from query B\r\n\r\n```bash\r\nREDASH_DOMAIN='yourdomain.com'\r\nA_QUERY_NUMBER='query number for query A'\r\nA_API_KEY_A='api key for query A'\r\nB_RESULT_NUMBER='query result number for query b'\r\n\r\n# this will download query B's results using query A's access key\r\nwget \\\r\n  -O query_b_results.csv \\\r\n  \"https://$REDASH_DOMAIN/api/queries/$A_QUERY_NUMBER/results/$B_RESULT_NUMBER.csv?api_key=$A_API_KEY\"\r\n```\r\n\r\nThis is a bug because one query's API key should NOT be able to access another query's results.\r\n\r\n### Technical details:\r\n\r\n* Redash Version: 1.0.3\r\n* Browser/OS: (Command Line) / Linux Mint 18.2\r\n* How did you install Redash: Command line\r\n\n", "before_files": [{"content": "import logging\nimport json\nimport time\n\nimport pystache\nfrom flask import make_response, request\nfrom flask_login import current_user\nfrom flask_restful import abort\nfrom redash import models, settings, utils\nfrom redash.tasks import QueryTask, record_event\nfrom redash.permissions import require_permission, not_view_only, has_access, require_access, view_only\nfrom redash.handlers.base import BaseResource, get_object_or_404\nfrom redash.utils import collect_query_parameters, collect_parameters_from_request, gen_query_hash\nfrom redash.tasks.queries import enqueue_query\n\n\ndef error_response(message):\n    return {'job': {'status': 4, 'error': message}}, 400\n\n\n#\n# Run a parameterized query synchronously and return the result\n# DISCLAIMER: Temporary solution to support parameters in queries. Should be\n#             removed once we refactor the query results API endpoints and handling\n#             on the client side. Please don't reuse in other API handlers.\n#\ndef run_query_sync(data_source, parameter_values, query_text, max_age=0):\n    query_parameters = set(collect_query_parameters(query_text))\n    missing_params = set(query_parameters) - set(parameter_values.keys())\n    if missing_params:\n        raise Exception('Missing parameter value for: {}'.format(\", \".join(missing_params)))\n\n    if query_parameters:\n        query_text = pystache.render(query_text, parameter_values)\n\n    if max_age <= 0:\n        query_result = None\n    else:\n        query_result = models.QueryResult.get_latest(data_source, query_text, max_age)\n\n    query_hash = gen_query_hash(query_text)\n\n    if query_result:\n        logging.info(\"Returning cached result for query %s\" % query_hash)\n        return query_result\n\n    try:\n        started_at = time.time()\n        data, error = data_source.query_runner.run_query(query_text, current_user)\n\n        if error:\n            logging.info('got bak error')\n            logging.info(error)\n            return None\n\n        run_time = time.time() - started_at\n        query_result, updated_query_ids = models.QueryResult.store_result(data_source.org, data_source,\n                                                                              query_hash, query_text, data,\n                                                                              run_time, utils.utcnow())\n\n        models.db.session.commit()\n        return query_result\n    except Exception as e:\n        if max_age > 0:\n            abort(404, message=\"Unable to get result from the database, and no cached query result found.\")\n        else:\n            abort(503, message=\"Unable to get result from the database.\")\n        return None\n\ndef run_query(data_source, parameter_values, query_text, query_id, max_age=0):\n    query_parameters = set(collect_query_parameters(query_text))\n    missing_params = set(query_parameters) - set(parameter_values.keys())\n    if missing_params:\n        return error_response('Missing parameter value for: {}'.format(\", \".join(missing_params)))\n\n    if data_source.paused:\n        if data_source.pause_reason:\n            message = '{} is paused ({}). Please try later.'.format(data_source.name, data_source.pause_reason)\n        else:\n            message = '{} is paused. Please try later.'.format(data_source.name)\n\n        return error_response(message)\n\n    if query_parameters:\n        query_text = pystache.render(query_text, parameter_values)\n\n    if max_age == 0:\n        query_result = None\n    else:\n        query_result = models.QueryResult.get_latest(data_source, query_text, max_age)\n\n    if query_result:\n        return {'query_result': query_result.to_dict()}\n    else:\n        job = enqueue_query(query_text, data_source, current_user.id, metadata={\"Username\": current_user.email, \"Query ID\": query_id})\n        return {'job': job.to_dict()}\n\n\nclass QueryResultListResource(BaseResource):\n    @require_permission('execute_query')\n    def post(self):\n        \"\"\"\n        Execute a query (or retrieve recent results).\n\n        :qparam string query: The query text to execute\n        :qparam number query_id: The query object to update with the result (optional)\n        :qparam number max_age: If query results less than `max_age` seconds old are available, return them, otherwise execute the query; if omitted, always execute\n        :qparam number data_source_id: ID of data source to query\n        \"\"\"\n        params = request.get_json(force=True)\n        parameter_values = collect_parameters_from_request(request.args)\n\n        query = params['query']\n        max_age = int(params.get('max_age', -1))\n        query_id = params.get('query_id', 'adhoc')\n\n        data_source = models.DataSource.get_by_id_and_org(params.get('data_source_id'), self.current_org)\n\n        if not has_access(data_source.groups, self.current_user, not_view_only):\n            return {'job': {'status': 4, 'error': 'You do not have permission to run queries with this data source.'}}, 403\n\n        self.record_event({\n            'action': 'execute_query',\n            'timestamp': int(time.time()),\n            'object_id': data_source.id,\n            'object_type': 'data_source',\n            'query': query\n        })\n        return run_query(data_source, parameter_values, query, query_id, max_age)\n\n\nONE_YEAR = 60 * 60 * 24 * 365.25\n\n\nclass QueryResultResource(BaseResource):\n    @staticmethod\n    def add_cors_headers(headers):\n        if 'Origin' in request.headers:\n            origin = request.headers['Origin']\n\n            if set(['*', origin]) & settings.ACCESS_CONTROL_ALLOW_ORIGIN:\n                headers['Access-Control-Allow-Origin'] = origin\n                headers['Access-Control-Allow-Credentials'] = str(settings.ACCESS_CONTROL_ALLOW_CREDENTIALS).lower()\n\n    @require_permission('view_query')\n    def options(self, query_id=None, query_result_id=None, filetype='json'):\n        headers = {}\n        self.add_cors_headers(headers)\n\n        if settings.ACCESS_CONTROL_REQUEST_METHOD:\n            headers['Access-Control-Request-Method'] = settings.ACCESS_CONTROL_REQUEST_METHOD\n\n        if settings.ACCESS_CONTROL_ALLOW_HEADERS:\n            headers['Access-Control-Allow-Headers'] = settings.ACCESS_CONTROL_ALLOW_HEADERS\n\n        return make_response(\"\", 200, headers)\n\n    @require_permission('view_query')\n    def get(self, query_id=None, query_result_id=None, filetype='json'):\n        \"\"\"\n        Retrieve query results.\n\n        :param number query_id: The ID of the query whose results should be fetched\n        :param number query_result_id: the ID of the query result to fetch\n        :param string filetype: Format to return. One of 'json', 'xlsx', or 'csv'. Defaults to 'json'.\n\n        :<json number id: Query result ID\n        :<json string query: Query that produced this result\n        :<json string query_hash: Hash code for query text\n        :<json object data: Query output\n        :<json number data_source_id: ID of data source that produced this result\n        :<json number runtime: Length of execution time in seconds\n        :<json string retrieved_at: Query retrieval date/time, in ISO format\n        \"\"\"\n        # TODO:\n        # This method handles two cases: retrieving result by id & retrieving result by query id.\n        # They need to be split, as they have different logic (for example, retrieving by query id\n        # should check for query parameters and shouldn't cache the result).\n        should_cache = query_result_id is not None\n\n        parameter_values = collect_parameters_from_request(request.args)\n        max_age = int(request.args.get('maxAge', 0))\n\n        query_result = None\n\n        if query_result_id:\n            query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)\n        elif query_id is not None:\n            query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)\n\n            if query is not None:\n                if settings.ALLOW_PARAMETERS_IN_EMBEDS and parameter_values:\n                    query_result = run_query_sync(query.data_source, parameter_values, query.to_dict()['query'], max_age=max_age)\n                elif query.latest_query_data_id is not None:\n                    query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)\n\n        if query_result:\n            require_access(query_result.data_source.groups, self.current_user, view_only)\n\n            if isinstance(self.current_user, models.ApiUser):\n                event = {\n                    'user_id': None,\n                    'org_id': self.current_org.id,\n                    'action': 'api_get',\n                    'timestamp': int(time.time()),\n                    'api_key': self.current_user.name,\n                    'file_type': filetype,\n                    'user_agent': request.user_agent.string,\n                    'ip': request.remote_addr\n                }\n\n                if query_id:\n                    event['object_type'] = 'query'\n                    event['object_id'] = query_id\n                else:\n                    event['object_type'] = 'query_result'\n                    event['object_id'] = query_result_id\n\n                record_event.delay(event)\n\n            if filetype == 'json':\n                response = self.make_json_response(query_result)\n            elif filetype == 'xlsx':\n                response = self.make_excel_response(query_result)\n            else:\n                response = self.make_csv_response(query_result)\n\n            if len(settings.ACCESS_CONTROL_ALLOW_ORIGIN) > 0:\n                self.add_cors_headers(response.headers)\n\n            if should_cache:\n                response.headers.add_header('Cache-Control', 'max-age=%d' % ONE_YEAR)\n\n            return response\n\n        else:\n            abort(404, message='No cached result found for this query.')\n\n    def make_json_response(self, query_result):\n        data = json.dumps({'query_result': query_result.to_dict()}, cls=utils.JSONEncoder)\n        headers = {'Content-Type': \"application/json\"}\n        return make_response(data, 200, headers)\n\n    @staticmethod\n    def make_csv_response(query_result):\n        headers = {'Content-Type': \"text/csv; charset=UTF-8\"}\n        return make_response(query_result.make_csv_content(), 200, headers)\n\n    @staticmethod\n    def make_excel_response(query_result):\n        headers = {'Content-Type': \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"}\n        return make_response(query_result.make_excel_content(), 200, headers)\n\n\nclass JobResource(BaseResource):\n    def get(self, job_id):\n        \"\"\"\n        Retrieve info about a running query job.\n        \"\"\"\n        job = QueryTask(job_id=job_id)\n        return {'job': job.to_dict()}\n\n    def delete(self, job_id):\n        \"\"\"\n        Cancel a query job in progress.\n        \"\"\"\n        job = QueryTask(job_id=job_id)\n        job.cancel()\n", "path": "redash/handlers/query_results.py"}], "after_files": [{"content": "import logging\nimport json\nimport time\n\nimport pystache\nfrom flask import make_response, request\nfrom flask_login import current_user\nfrom flask_restful import abort\nfrom redash import models, settings, utils\nfrom redash.tasks import QueryTask, record_event\nfrom redash.permissions import require_permission, not_view_only, has_access, require_access, view_only\nfrom redash.handlers.base import BaseResource, get_object_or_404\nfrom redash.utils import collect_query_parameters, collect_parameters_from_request, gen_query_hash\nfrom redash.tasks.queries import enqueue_query\n\n\ndef error_response(message):\n    return {'job': {'status': 4, 'error': message}}, 400\n\n\n#\n# Run a parameterized query synchronously and return the result\n# DISCLAIMER: Temporary solution to support parameters in queries. Should be\n#             removed once we refactor the query results API endpoints and handling\n#             on the client side. Please don't reuse in other API handlers.\n#\ndef run_query_sync(data_source, parameter_values, query_text, max_age=0):\n    query_parameters = set(collect_query_parameters(query_text))\n    missing_params = set(query_parameters) - set(parameter_values.keys())\n    if missing_params:\n        raise Exception('Missing parameter value for: {}'.format(\", \".join(missing_params)))\n\n    if query_parameters:\n        query_text = pystache.render(query_text, parameter_values)\n\n    if max_age <= 0:\n        query_result = None\n    else:\n        query_result = models.QueryResult.get_latest(data_source, query_text, max_age)\n\n    query_hash = gen_query_hash(query_text)\n\n    if query_result:\n        logging.info(\"Returning cached result for query %s\" % query_hash)\n        return query_result\n\n    try:\n        started_at = time.time()\n        data, error = data_source.query_runner.run_query(query_text, current_user)\n\n        if error:\n            logging.info('got bak error')\n            logging.info(error)\n            return None\n\n        run_time = time.time() - started_at\n        query_result, updated_query_ids = models.QueryResult.store_result(data_source.org, data_source,\n                                                                              query_hash, query_text, data,\n                                                                              run_time, utils.utcnow())\n\n        models.db.session.commit()\n        return query_result\n    except Exception as e:\n        if max_age > 0:\n            abort(404, message=\"Unable to get result from the database, and no cached query result found.\")\n        else:\n            abort(503, message=\"Unable to get result from the database.\")\n        return None\n\ndef run_query(data_source, parameter_values, query_text, query_id, max_age=0):\n    query_parameters = set(collect_query_parameters(query_text))\n    missing_params = set(query_parameters) - set(parameter_values.keys())\n    if missing_params:\n        return error_response('Missing parameter value for: {}'.format(\", \".join(missing_params)))\n\n    if data_source.paused:\n        if data_source.pause_reason:\n            message = '{} is paused ({}). Please try later.'.format(data_source.name, data_source.pause_reason)\n        else:\n            message = '{} is paused. Please try later.'.format(data_source.name)\n\n        return error_response(message)\n\n    if query_parameters:\n        query_text = pystache.render(query_text, parameter_values)\n\n    if max_age == 0:\n        query_result = None\n    else:\n        query_result = models.QueryResult.get_latest(data_source, query_text, max_age)\n\n    if query_result:\n        return {'query_result': query_result.to_dict()}\n    else:\n        job = enqueue_query(query_text, data_source, current_user.id, metadata={\"Username\": current_user.email, \"Query ID\": query_id})\n        return {'job': job.to_dict()}\n\n\nclass QueryResultListResource(BaseResource):\n    @require_permission('execute_query')\n    def post(self):\n        \"\"\"\n        Execute a query (or retrieve recent results).\n\n        :qparam string query: The query text to execute\n        :qparam number query_id: The query object to update with the result (optional)\n        :qparam number max_age: If query results less than `max_age` seconds old are available, return them, otherwise execute the query; if omitted, always execute\n        :qparam number data_source_id: ID of data source to query\n        \"\"\"\n        params = request.get_json(force=True)\n        parameter_values = collect_parameters_from_request(request.args)\n\n        query = params['query']\n        max_age = int(params.get('max_age', -1))\n        query_id = params.get('query_id', 'adhoc')\n\n        data_source = models.DataSource.get_by_id_and_org(params.get('data_source_id'), self.current_org)\n\n        if not has_access(data_source.groups, self.current_user, not_view_only):\n            return {'job': {'status': 4, 'error': 'You do not have permission to run queries with this data source.'}}, 403\n\n        self.record_event({\n            'action': 'execute_query',\n            'timestamp': int(time.time()),\n            'object_id': data_source.id,\n            'object_type': 'data_source',\n            'query': query\n        })\n        return run_query(data_source, parameter_values, query, query_id, max_age)\n\n\nONE_YEAR = 60 * 60 * 24 * 365.25\n\n\nclass QueryResultResource(BaseResource):\n    @staticmethod\n    def add_cors_headers(headers):\n        if 'Origin' in request.headers:\n            origin = request.headers['Origin']\n\n            if set(['*', origin]) & settings.ACCESS_CONTROL_ALLOW_ORIGIN:\n                headers['Access-Control-Allow-Origin'] = origin\n                headers['Access-Control-Allow-Credentials'] = str(settings.ACCESS_CONTROL_ALLOW_CREDENTIALS).lower()\n\n    @require_permission('view_query')\n    def options(self, query_id=None, query_result_id=None, filetype='json'):\n        headers = {}\n        self.add_cors_headers(headers)\n\n        if settings.ACCESS_CONTROL_REQUEST_METHOD:\n            headers['Access-Control-Request-Method'] = settings.ACCESS_CONTROL_REQUEST_METHOD\n\n        if settings.ACCESS_CONTROL_ALLOW_HEADERS:\n            headers['Access-Control-Allow-Headers'] = settings.ACCESS_CONTROL_ALLOW_HEADERS\n\n        return make_response(\"\", 200, headers)\n\n    @require_permission('view_query')\n    def get(self, query_id=None, query_result_id=None, filetype='json'):\n        \"\"\"\n        Retrieve query results.\n\n        :param number query_id: The ID of the query whose results should be fetched\n        :param number query_result_id: the ID of the query result to fetch\n        :param string filetype: Format to return. One of 'json', 'xlsx', or 'csv'. Defaults to 'json'.\n\n        :<json number id: Query result ID\n        :<json string query: Query that produced this result\n        :<json string query_hash: Hash code for query text\n        :<json object data: Query output\n        :<json number data_source_id: ID of data source that produced this result\n        :<json number runtime: Length of execution time in seconds\n        :<json string retrieved_at: Query retrieval date/time, in ISO format\n        \"\"\"\n        # TODO:\n        # This method handles two cases: retrieving result by id & retrieving result by query id.\n        # They need to be split, as they have different logic (for example, retrieving by query id\n        # should check for query parameters and shouldn't cache the result).\n        should_cache = query_result_id is not None\n\n        parameter_values = collect_parameters_from_request(request.args)\n        max_age = int(request.args.get('maxAge', 0))\n\n        query_result = None\n\n        if query_result_id:\n            query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)\n\n        if query_id is not None:\n            query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)\n\n            if query_result is None and query is not None:\n                if settings.ALLOW_PARAMETERS_IN_EMBEDS and parameter_values:\n                    query_result = run_query_sync(query.data_source, parameter_values, query.to_dict()['query'], max_age=max_age)\n                elif query.latest_query_data_id is not None:\n                    query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)\n                \n            if query is not None and query_result is not None and self.current_user.is_api_user():\n                if query.query_hash != query_result.query_hash:\n                    abort(404, message='No cached result found for this query.')\n\n        if query_result:\n            require_access(query_result.data_source.groups, self.current_user, view_only)\n\n            if isinstance(self.current_user, models.ApiUser):\n                event = {\n                    'user_id': None,\n                    'org_id': self.current_org.id,\n                    'action': 'api_get',\n                    'timestamp': int(time.time()),\n                    'api_key': self.current_user.name,\n                    'file_type': filetype,\n                    'user_agent': request.user_agent.string,\n                    'ip': request.remote_addr\n                }\n\n                if query_id:\n                    event['object_type'] = 'query'\n                    event['object_id'] = query_id\n                else:\n                    event['object_type'] = 'query_result'\n                    event['object_id'] = query_result_id\n\n                record_event.delay(event)\n\n            if filetype == 'json':\n                response = self.make_json_response(query_result)\n            elif filetype == 'xlsx':\n                response = self.make_excel_response(query_result)\n            else:\n                response = self.make_csv_response(query_result)\n\n            if len(settings.ACCESS_CONTROL_ALLOW_ORIGIN) > 0:\n                self.add_cors_headers(response.headers)\n\n            if should_cache:\n                response.headers.add_header('Cache-Control', 'private,max-age=%d' % ONE_YEAR)\n\n            return response\n\n        else:\n            abort(404, message='No cached result found for this query.')\n\n    def make_json_response(self, query_result):\n        data = json.dumps({'query_result': query_result.to_dict()}, cls=utils.JSONEncoder)\n        headers = {'Content-Type': \"application/json\"}\n        return make_response(data, 200, headers)\n\n    @staticmethod\n    def make_csv_response(query_result):\n        headers = {'Content-Type': \"text/csv; charset=UTF-8\"}\n        return make_response(query_result.make_csv_content(), 200, headers)\n\n    @staticmethod\n    def make_excel_response(query_result):\n        headers = {'Content-Type': \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"}\n        return make_response(query_result.make_excel_content(), 200, headers)\n\n\nclass JobResource(BaseResource):\n    def get(self, job_id):\n        \"\"\"\n        Retrieve info about a running query job.\n        \"\"\"\n        job = QueryTask(job_id=job_id)\n        return {'job': job.to_dict()}\n\n    def delete(self, job_id):\n        \"\"\"\n        Cancel a query job in progress.\n        \"\"\"\n        job = QueryTask(job_id=job_id)\n        job.cancel()\n", "path": "redash/handlers/query_results.py"}]}