Repository: getredash/redash

Issue:
Dashboards list includes dashboards with text widgets where you have no access to any other widgets
in this case, a use named A, he has not have any datasource access or view permission, while the dashboard have a text widget, the user A can find the dashboard which have text widget on dashboard lis page, the reason is than this python file: app/redash/model/__init__.py about the class Dashboard with that method :

```python
    @classmethod
    def all(cls, org, group_ids, user_id):
        query = (
            Dashboard.query
            .options(
                subqueryload(Dashboard.user).load_only('_profile_image_url', 'name'),
            )
            .outerjoin(Widget)
            .outerjoin(Visualization)
            .outerjoin(Query)
            .outerjoin(DataSourceGroup, Query.data_source_id == DataSourceGroup.data_source_id)
            .filter(
                Dashboard.is_archived == False,
                (DataSourceGroup.group_id.in_(group_ids) |
                 (Dashboard.user_id == user_id)
                  ((Widget.dashboard != None) & (Widget.visualization == None))),
                Dashboard.org == org)
            .distinct())

        query = query.filter(or_(Dashboard.user_id == user_id, Dashboard.is_draft == False))

        return query
```

the query on Widget.visualization == None so will find all dashboard that have text widget

* Redash Version: 6.0.7.0.8.0
* Browser/OS: chrome/ios
* How did you install Redash: k8s

Dashboards list includes dashboards with text widgets where you have no access to any other widgets
in this case, a use named A, he has not have any datasource access or view permission, while the dashboard have a text widget, the user A can find the dashboard which have text widget on dashboard lis page, the reason is than this python file: app/redash/model/__init__.py about the class Dashboard with that method :

```python
    @classmethod
    def all(cls, org, group_ids, user_id):
        query = (
            Dashboard.query
            .options(
                subqueryload(Dashboard.user).load_only('_profile_image_url', 'name'),
            )
            .outerjoin(Widget)
            .outerjoin(Visualization)
            .outerjoin(Query)
            .outerjoin(DataSourceGroup, Query.data_source_id == DataSourceGroup.data_source_id)
            .filter(
                Dashboard.is_archived == False,
                (DataSourceGroup.group_id.in_(group_ids) |
                 (Dashboard.user_id == user_id)
                  ((Widget.dashboard != None) & (Widget.visualization == None))),
                Dashboard.org == org)
            .distinct())

        query = query.filter(or_(Dashboard.user_id == user_id, Dashboard.is_draft == False))

        return query
```

the query on Widget.visualization == None so will find all dashboard that have text widget

* Redash Version: 6.0.7.0.8.0
* Browser/OS: chrome/ios
* How did you install Redash: k8s



Discussion:
It was implemented this way intentionally, but we no longer need this behavior. You're welcome to submit a Pull Request to update this behavior.
It was implemented this way intentionally, but we no longer need this behavior. You're welcome to submit a Pull Request to update this behavior.

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
@@ -11,6 +11,7 @@
     not_view_only,
     require_access,
     require_permission,
+    require_any_of_permission,
     view_only,
 )
 from redash.tasks import QueryTask
@@ -220,7 +221,7 @@ def add_cors_headers(headers):
                     settings.ACCESS_CONTROL_ALLOW_CREDENTIALS
                 ).lower()
 
-    @require_permission("view_query")
+    @require_any_of_permission(("view_query", "execute_query"))
     def options(self, query_id=None, query_result_id=None, filetype="json"):
         headers = {}
         self.add_cors_headers(headers)
@@ -237,7 +238,7 @@ def options(self, query_id=None, query_result_id=None, filetype="json"):
 
         return make_response("", 200, headers)
 
-    @require_permission("view_query")
+    @require_any_of_permission(("view_query", "execute_query"))
     def post(self, query_id):
         """
         Execute a saved query.
@@ -283,7 +284,7 @@ def post(self, query_id):
             else:
                 return error_messages["no_permission"]
 
-    @require_permission("view_query")
+    @require_any_of_permission(("view_query", "execute_query"))
     def get(self, query_id=None, query_result_id=None, filetype="json"):
         """
         Retrieve query results.
diff --git a/redash/models/__init__.py b/redash/models/__init__.py
--- a/redash/models/__init__.py
+++ b/redash/models/__init__.py
@@ -1072,7 +1072,6 @@ def all(cls, org, group_ids, user_id):
                 (
                     DataSourceGroup.group_id.in_(group_ids)
                     | (Dashboard.user_id == user_id)
-                    | ((Widget.dashboard != None) & (Widget.visualization == None))
                 ),
                 Dashboard.org == org,
             )
diff --git a/redash/permissions.py b/redash/permissions.py
--- a/redash/permissions.py
+++ b/redash/permissions.py
@@ -55,13 +55,17 @@ def require_access(obj, user, need_view_only):
 
 
 class require_permissions(object):
-    def __init__(self, permissions):
+    def __init__(self, permissions, allow_one=False):
         self.permissions = permissions
+        self.allow_one = allow_one
 
     def __call__(self, fn):
         @functools.wraps(fn)
         def decorated(*args, **kwargs):
-            has_permissions = current_user.has_permissions(self.permissions)
+            if self.allow_one:
+                has_permissions = any([current_user.has_permission(permission) for permission in self.permissions])
+            else:
+                has_permissions = current_user.has_permissions(self.permissions)
 
             if has_permissions:
                 return fn(*args, **kwargs)
@@ -75,6 +79,10 @@ def require_permission(permission):
     return require_permissions((permission,))
 
 
+def require_any_of_permission(permissions):
+    return require_permissions(permissions, True)
+
+
 def require_admin(fn):
     return require_permission("admin")(fn)
 


Test patch:
diff --git a/tests/test_models.py b/tests/test_models.py
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -736,24 +736,38 @@ def test_returns_dashboards_created_by_user(self):
             d1, list(models.Dashboard.all(self.u2.org, self.u2.group_ids, self.u2.id))
         )
 
-    def test_returns_dashboards_with_text_widgets(self):
+    def test_returns_dashboards_with_text_widgets_to_creator(self):
         w1 = self.factory.create_widget(visualization=None)
 
+        self.assertEqual(w1.dashboard.user, self.factory.user)
         self.assertIn(
-            w1.dashboard, models.Dashboard.all(self.u1.org, self.u1.group_ids, None)
+            w1.dashboard,
+            list(
+                models.Dashboard.all(
+                    self.factory.user.org,
+                    self.factory.user.group_ids,
+                    self.factory.user.id,
+                )
+            ),
         )
-        self.assertIn(
-            w1.dashboard, models.Dashboard.all(self.u2.org, self.u2.group_ids, None)
+        self.assertNotIn(
+            w1.dashboard,
+            list(models.Dashboard.all(self.u1.org, self.u1.group_ids, self.u1.id)),
         )
 
     def test_returns_dashboards_from_current_org_only(self):
-        w1 = self.factory.create_widget(visualization=None)
+        w1 = self.factory.create_widget()
 
         user = self.factory.create_user(org=self.factory.create_org())
 
         self.assertIn(
-            w1.dashboard, models.Dashboard.all(self.u1.org, self.u1.group_ids, None)
+            w1.dashboard,
+            list(
+                models.Dashboard.all(
+                    self.factory.user.org, self.factory.user.group_ids, None
+                )
+            ),
         )
         self.assertNotIn(
-            w1.dashboard, models.Dashboard.all(user.org, user.group_ids, None)
+            w1.dashboard, list(models.Dashboard.all(user.org, user.group_ids, user.id))
         )