You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by su...@apache.org on 2021/12/11 09:47:47 UTC

[superset] 01/01: helper methods and dashboard access

This is an automated email from the ASF dual-hosted git repository.

suddjian pushed a commit to branch guest-token-authz
in repository https://gitbox.apache.org/repos/asf/superset.git

commit d68734041224f4e90a542635f328fb7bed693c7d
Author: David Aaron Suddjian <aa...@gmail.com>
AuthorDate: Tue Dec 7 14:13:44 2021 -0800

    helper methods and dashboard access
---
 superset/common/request_contexed_based.py | 16 +---------------
 superset/config.py                        |  2 +-
 superset/dashboards/filters.py            | 24 +++++++++++++++++++-----
 superset/security/api.py                  |  2 +-
 superset/security/guest_token.py          | 14 +++++++++++---
 superset/security/manager.py              | 23 +++++++++++++++++------
 superset/views/base.py                    |  9 +--------
 superset/views/core.py                    |  5 +++--
 8 files changed, 54 insertions(+), 41 deletions(-)

diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py
index 0b06a0c..5d8405e 100644
--- a/superset/common/request_contexed_based.py
+++ b/superset/common/request_contexed_based.py
@@ -16,24 +16,10 @@
 # under the License.
 from __future__ import annotations
 
-from typing import List, TYPE_CHECKING
-
-from flask import g
-
 from superset import conf, security_manager
 
-if TYPE_CHECKING:
-    from flask_appbuilder.security.sqla.models import Role
-
-
-def get_user_roles() -> List[Role]:
-    if g.user.is_anonymous:
-        public_role = conf.get("AUTH_ROLE_PUBLIC")
-        return [security_manager.get_public_role()] if public_role else []
-    return g.user.roles
-
 
 def is_user_admin() -> bool:
-    user_roles = [role.name.lower() for role in get_user_roles()]
+    user_roles = [role.name.lower() for role in security_manager.get_user_roles()]
     admin_role = conf.get("AUTH_ROLE_ADMIN").lower()
     return admin_role in user_roles
diff --git a/superset/config.py b/superset/config.py
index f9b7343..2ff2f1a 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -389,7 +389,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
     # a custom security config could potentially give access to setting filters on
     # tables that users do not have access to.
     "ROW_LEVEL_SECURITY": True,
-    "EMBEDDED_SUPERSET": False,
+    "EMBEDDED_SUPERSET": False,  # This requires that the public role be available
     # Enables Alerts and reports new implementation
     "ALERT_REPORTS": False,
     # Enable experimental feature to search for other dashboards
diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py
index 9658478..3d49978 100644
--- a/superset/dashboards/filters.py
+++ b/superset/dashboards/filters.py
@@ -16,6 +16,7 @@
 # under the License.
 from typing import Any, Optional
 
+from flask import g
 from flask_appbuilder.security.sqla.models import Role
 from flask_babel import lazy_gettext as _
 from sqlalchemy import and_, or_
@@ -25,7 +26,8 @@ from superset import db, is_feature_enabled, security_manager
 from superset.models.core import FavStar
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
-from superset.views.base import BaseFilter, get_user_roles, is_user_admin
+from superset.security.guest_token import GuestTokenResourceType, GuestUser
+from superset.views.base import BaseFilter, is_user_admin
 from superset.views.base_api import BaseFavoriteFilter
 
 
@@ -112,7 +114,7 @@ class DashboardAccessFilter(BaseFilter):  # pylint: disable=too-few-public-metho
             )
         )
 
-        dashboard_rbac_or_filters = []
+        feature_flagged_filters = []
         if is_feature_enabled("DASHBOARD_RBAC"):
             roles_based_query = (
                 db.session.query(Dashboard.id)
@@ -121,19 +123,31 @@ class DashboardAccessFilter(BaseFilter):  # pylint: disable=too-few-public-metho
                     and_(
                         Dashboard.published.is_(True),
                         dashboard_has_roles,
-                        Role.id.in_([x.id for x in get_user_roles()]),
+                        Role.id.in_([x.id for x in security_manager.get_user_roles()]),
                     ),
                 )
             )
 
-            dashboard_rbac_or_filters.append(Dashboard.id.in_(roles_based_query))
+            feature_flagged_filters.append(Dashboard.id.in_(roles_based_query))
+
+        if is_feature_enabled("EMBEDDED_SUPERSET") and security_manager.is_guest_user(
+            g.user
+        ):
+            guest_user: GuestUser = g.user
+            embedded_dashboard_ids = [
+                r["id"]
+                for r in guest_user.resources
+                if r["type"] == GuestTokenResourceType.DASHBOARD
+            ]
+            if len(embedded_dashboard_ids) != 0:
+                feature_flagged_filters.append(Dashboard.id.in_(embedded_dashboard_ids))
 
         query = query.filter(
             or_(
                 Dashboard.id.in_(owner_ids_query),
                 Dashboard.id.in_(datasource_perm_query),
                 Dashboard.id.in_(users_favorite_dash_query),
-                *dashboard_rbac_or_filters,
+                *feature_flagged_filters,
             )
         )
 
diff --git a/superset/security/api.py b/superset/security/api.py
index c0a4a77..54efcd0 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -35,7 +35,7 @@ class UserSchema(Schema):
 
 
 class ResourceSchema(Schema):
-    type = fields.String(required=True)
+    type = fields.String(required=True)  # todo figure out how to make this an enum
     id = fields.String(required=True)
     rls = fields.String()
 
diff --git a/superset/security/guest_token.py b/superset/security/guest_token.py
index cbef52f..60add81 100644
--- a/superset/security/guest_token.py
+++ b/superset/security/guest_token.py
@@ -14,6 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+from enum import Enum
 from typing import List, Optional, TypedDict, Union
 
 from flask_appbuilder.security.sqla.models import Role
@@ -26,17 +27,24 @@ class GuestTokenUser(TypedDict, total=False):
     last_name: str
 
 
+class GuestTokenResourceType(Enum):
+    DASHBOARD = "dashboard"
+
+
 class GuestTokenResource(TypedDict):
-    type: str
+    type: GuestTokenResourceType
     id: Union[str, int]
     rls: Optional[str]
 
 
+GuestTokenResources = List[GuestTokenResource]
+
+
 class GuestToken(TypedDict):
     iat: float
     exp: float
     user: GuestTokenUser
-    resources: List[GuestTokenResource]
+    resources: GuestTokenResources
 
 
 class GuestUser(AnonymousUserMixin):
@@ -50,7 +58,7 @@ class GuestUser(AnonymousUserMixin):
     def is_authenticated(self) -> bool:
         """
         This is set to true because guest users should be considered authenticated,
-        at least in most places. The treatment of this flag is pretty inconsistent.
+        at least in most places. The treatment of this flag is kind of inconsistent.
         """
         return True
 
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 96363d2..95bcbeb 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -68,6 +68,7 @@ from superset.exceptions import SupersetSecurityException
 from superset.security.guest_token import (
     GuestToken,
     GuestTokenResource,
+    GuestTokenResources,
     GuestTokenUser,
     GuestUser,
 )
@@ -278,7 +279,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         """
 
         user = g.user
-        if user.is_anonymous:
+        if user.is_anonymous and not self.is_guest_user(user):
             return self.is_item_public(permission_name, view_name)
         return self._has_view_access(user, permission_name, view_name)
 
@@ -1097,6 +1098,12 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
     def get_anonymous_user(self) -> User:  # pylint: disable=no-self-use
         return AnonymousUserMixin()
 
+    def get_user_roles(self) -> List[Role]:
+        if g.user.is_anonymous:
+            public_role = current_app.config.get("AUTH_ROLE_PUBLIC")
+            return [self.get_public_role()] if public_role else []
+        return g.user.roles
+
     def get_rls_filters(self, table: "BaseDatasource") -> List[SqlaQuery]:
         """
         Retrieves the appropriate row level security filters for the current user and
@@ -1195,8 +1202,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
                 )
             )
 
-    @staticmethod
-    def raise_for_dashboard_access(dashboard: "Dashboard") -> None:
+    def raise_for_dashboard_access(self, dashboard: "Dashboard") -> None:
         """
         Raise an exception if the user cannot access the dashboard.
 
@@ -1206,14 +1212,15 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         # pylint: disable=import-outside-toplevel
         from superset import is_feature_enabled
         from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
-        from superset.views.base import get_user_roles, is_user_admin
+        from superset.views.base import is_user_admin
         from superset.views.utils import is_owner
 
         has_rbac_access = True
 
         if is_feature_enabled("DASHBOARD_RBAC"):
             has_rbac_access = any(
-                dashboard_role.id in [user_role.id for user_role in get_user_roles()]
+                dashboard_role.id
+                in [user_role.id for user_role in self.get_user_roles()]
                 for dashboard_role in dashboard.roles
             )
 
@@ -1255,7 +1262,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         return time.time()
 
     def create_guest_access_token(
-        self, user: GuestTokenUser, resources: List[GuestTokenResource]
+        self, user: GuestTokenUser, resources: GuestTokenResources
     ) -> bytes:
         secret = current_app.config["GUEST_TOKEN_JWT_SECRET"]
         algo = current_app.config["GUEST_TOKEN_JWT_ALGO"]
@@ -1319,3 +1326,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         if token.get("resources") is None:
             raise ValueError("Guest token does not contain a resources claim")
         return cast(GuestToken, token)
+
+    @staticmethod
+    def is_guest_user(user: Any) -> bool:
+        return hasattr(user, "is_guest_user") and user.is_guest_user
diff --git a/superset/views/base.py b/superset/views/base.py
index 72dc805..e5fc082 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -263,15 +263,8 @@ def create_table_permissions(table: models.SqlaTable) -> None:
         security_manager.add_permission_view_menu("schema_access", table.schema_perm)
 
 
-def get_user_roles() -> List[Role]:
-    if g.user.is_anonymous:
-        public_role = conf.get("AUTH_ROLE_PUBLIC")
-        return [security_manager.find_role(public_role)] if public_role else []
-    return g.user.roles
-
-
 def is_user_admin() -> bool:
-    user_roles = [role.name.lower() for role in list(get_user_roles())]
+    user_roles = [role.name.lower() for role in list(security_manager.get_user_roles())]
     return "admin" in user_roles
 
 
diff --git a/superset/views/core.py b/superset/views/core.py
index 9557c30..68f6e78 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -135,7 +135,6 @@ from superset.views.base import (
     data_payload_response,
     generate_download_headers,
     get_error_msg,
-    get_user_roles,
     handle_api_exception,
     json_error_response,
     json_errors_response,
@@ -1886,7 +1885,9 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
                 f"ERROR: cannot find dashboard {dashboard_id}", status=404
             )
 
-        edit_perm = is_owner(dash, g.user) or admin_role in get_user_roles()
+        edit_perm = (
+            is_owner(dash, g.user) or admin_role in security_manager.get_user_roles()
+        )
         if not edit_perm:
             username = g.user.username if hasattr(g.user, "username") else "user"
             return json_error_response(