Repository: getredash/redash

Issue:
Cannot Add Query-based Dropdown Parameter to a Saved Query
<!--

We use GitHub only for bug reports 🐛

Anything else should be posted to https://discuss.redash.io 👫

🚨For support, help & questions use https://discuss.redash.io/c/support
💡For feature requests & ideas use https://discuss.redash.io/c/feature-requests

**Found a security vulnerability?** Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use this PGP key.

-->

### Issue Summary

On a new query, query-based dropdown parameters get their values from `/api/queries/:id/dropdown` and on saved queries they get them by association from their parent query id (`/api/queries/:parent_id/dropdowns/:id`).

When adding a query-based dropdown parameter to a saved query, the association is not yet created which results in a 403.

### Steps to Reproduce

1. Create a new query without a parameter
2. Save it
3. Add a query-based parameter to the query.
4. The dropdown is not populated with values (a 403 occurs)



Discussion:

Resolving patch:
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
@@ -108,18 +108,22 @@ def post(self):
 
 class QueryResultDropdownResource(BaseResource):
     def get(self, query_id):
+        query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
+        require_access(query.data_source, current_user, view_only)
         return dropdown_values(query_id)
 
 
 class QueryDropdownsResource(BaseResource):
     def get(self, query_id, dropdown_query_id):
         query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
+        require_access(query, current_user, view_only)
 
         related_queries_ids = [p['queryId'] for p in query.parameters if p['type'] == 'query']
         if int(dropdown_query_id) not in related_queries_ids:
-            abort(403)
+            dropdown_query = get_object_or_404(models.Query.get_by_id_and_org, dropdown_query_id, self.current_org)
+            require_access(dropdown_query.data_source, current_user, view_only)
 
-        return dropdown_values(dropdown_query_id, should_require_access=False)
+        return dropdown_values(dropdown_query_id)
 
 
 class QueryResultResource(BaseResource):
diff --git a/redash/models/parameterized_query.py b/redash/models/parameterized_query.py
--- a/redash/models/parameterized_query.py
+++ b/redash/models/parameterized_query.py
@@ -1,6 +1,5 @@
 import pystache
 from functools import partial
-from flask_login import current_user
 from flask_restful import abort
 from numbers import Number
 from redash.utils import mustache_render, json_loads
@@ -17,27 +16,21 @@ def _pluck_name_and_value(default_column, row):
     return {"name": row[name_column], "value": unicode(row[value_column])}
 
 
-def _load_result(query_id, should_require_access):
+def _load_result(query_id):
     from redash.authentication.org_resolving import current_org
     from redash import models
 
     query = models.Query.get_by_id_and_org(query_id, current_org)
 
-    if should_require_access:
-        require_access(query.data_source, current_user, view_only)
-
-    query_result = models.QueryResult.get_by_id_and_org(query.latest_query_data_id, current_org)
-
     if query.data_source:
-        require_access(query.data_source.groups, current_user, view_only)
         query_result = models.QueryResult.get_by_id_and_org(query.latest_query_data_id, current_org)
         return json_loads(query_result.data)
     else:
         abort(400, message="This query is detached from any data source. Please select a different query.")
 
 
-def dropdown_values(query_id, should_require_access=True):
-    data = _load_result(query_id, should_require_access)
+def dropdown_values(query_id):
+    data = _load_result(query_id)
     first_column = data["columns"][0]["name"]
     pluck = partial(_pluck_name_and_value, first_column)
     return map(pluck, data["rows"])


Test patch:
diff --git a/tests/handlers/test_query_results.py b/tests/handlers/test_query_results.py
--- a/tests/handlers/test_query_results.py
+++ b/tests/handlers/test_query_results.py
@@ -201,26 +201,39 @@ def test_checks_for_access_to_the_query(self):
 
         self.assertEquals(rv.status_code, 403)
 
+
 class TestQueryDropdownsResource(BaseTestCase):
-    def test_prevents_access_if_query_isnt_associated_with_parent(self):
+    def test_prevents_access_if_unassociated_and_doesnt_have_access(self):
         query = self.factory.create_query()
-        unrelated_dropdown_query = self.factory.create_query()
+        ds2 = self.factory.create_data_source(group=self.factory.org.admin_group, view_only=False)
+        unrelated_dropdown_query = self.factory.create_query(data_source=ds2)
+
+        # unrelated_dropdown_query has not been associated with query
+        # user does not have direct access to unrelated_dropdown_query
 
         rv = self.make_request('get', '/api/queries/{}/dropdowns/{}'.format(query.id, unrelated_dropdown_query.id))
 
         self.assertEquals(rv.status_code, 403)
 
-
-class TestQueryDropdownsResource(BaseTestCase):
-    def test_prevents_access_if_query_isnt_associated_with_parent(self):
+    def test_allows_access_if_unassociated_but_user_has_access(self):
         query = self.factory.create_query()
-        unrelated_dropdown_query = self.factory.create_query()
+
+        query_result = self.factory.create_query_result()
+        data = {
+            'rows': [],
+            'columns': [{'name': 'whatever'}]
+        }
+        query_result = self.factory.create_query_result(data=json_dumps(data))
+        unrelated_dropdown_query = self.factory.create_query(latest_query_data=query_result)
+
+        # unrelated_dropdown_query has not been associated with query
+        # user has direct access to unrelated_dropdown_query
 
         rv = self.make_request('get', '/api/queries/{}/dropdowns/{}'.format(query.id, unrelated_dropdown_query.id))
 
-        self.assertEquals(rv.status_code, 403)
+        self.assertEquals(rv.status_code, 200)
 
-    def test_allows_access_if_user_has_access_to_parent_query(self):
+    def test_allows_access_if_associated_and_has_access_to_parent(self):
         query_result = self.factory.create_query_result()
         data = {
             'rows': [],
@@ -237,22 +250,28 @@ def test_allows_access_if_user_has_access_to_parent_query(self):
         }
         query = self.factory.create_query(options=options)
 
+        # dropdown_query has been associated with query
+        # user has access to query
+
         rv = self.make_request('get', '/api/queries/{}/dropdowns/{}'.format(query.id, dropdown_query.id))
 
         self.assertEquals(rv.status_code, 200)
 
-    def test_prevents_access_if_user_doesnt_have_access_to_parent_query(self):
-        related_dropdown_query = self.factory.create_query()
-        unrelated_dropdown_query = self.factory.create_query()
+    def test_prevents_access_if_associated_and_doesnt_have_access_to_parent(self):
+        ds2 = self.factory.create_data_source(group=self.factory.org.admin_group, view_only=False)
+        dropdown_query = self.factory.create_query(data_source=ds2)
         options = {
                 'parameters': [{
                 'type': 'query',
-                'queryId': related_dropdown_query.id
+                'queryId': dropdown_query.id
             }]
         }
-        query = self.factory.create_query(options=options)
+        query = self.factory.create_query(data_source=ds2, options=options)
 
-        rv = self.make_request('get', '/api/queries/{}/dropdowns/{}'.format(query.id, unrelated_dropdown_query.id))
+        # dropdown_query has been associated with query
+        # user doesnt have access to either query
+
+        rv = self.make_request('get', '/api/queries/{}/dropdowns/{}'.format(query.id, dropdown_query.id))
 
         self.assertEquals(rv.status_code, 403)