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/15 04:39:58 UTC
[superset] 01/04: 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 a21a4175c3fc1ff5c4189bd782be747e0de5a108
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 1a0a453..6bd31f2 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(