mirror of
https://github.com/apache/superset.git
synced 2026-04-21 17:14:57 +00:00
feat: Explorable protocol (#36245)
This commit is contained in:
@@ -87,6 +87,7 @@ if TYPE_CHECKING:
|
||||
RowLevelSecurityFilter,
|
||||
SqlaTable,
|
||||
)
|
||||
from superset.explorables.base import Explorable
|
||||
from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
@@ -540,24 +541,43 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
or (catalog_perm and self.can_access("catalog_access", catalog_perm))
|
||||
)
|
||||
|
||||
def can_access_schema(self, datasource: "BaseDatasource") -> bool:
|
||||
def can_access_schema(self, datasource: "BaseDatasource | Explorable") -> bool:
|
||||
"""
|
||||
Return True if the user can access the schema associated with specified
|
||||
datasource, False otherwise.
|
||||
|
||||
For SQL datasources: Checks database → catalog → schema hierarchy
|
||||
For other explorables: Only checks all_datasources permission
|
||||
|
||||
:param datasource: The datasource
|
||||
:returns: Whether the user can access the datasource's schema
|
||||
"""
|
||||
from superset.connectors.sqla.models import BaseDatasource
|
||||
|
||||
return (
|
||||
self.can_access_all_datasources()
|
||||
or self.can_access_database(datasource.database)
|
||||
or (
|
||||
datasource.catalog
|
||||
# Admin/superuser override
|
||||
if self.can_access_all_datasources():
|
||||
return True
|
||||
|
||||
# SQL-specific hierarchy checks
|
||||
if isinstance(datasource, BaseDatasource):
|
||||
# Database-level access grants all schemas
|
||||
if self.can_access_database(datasource.database):
|
||||
return True
|
||||
|
||||
# Catalog-level access grants all schemas in catalog
|
||||
if (
|
||||
hasattr(datasource, "catalog")
|
||||
and datasource.catalog
|
||||
and self.can_access_catalog(datasource.database, datasource.catalog)
|
||||
)
|
||||
or self.can_access("schema_access", datasource.schema_perm or "")
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
# Schema-level permission (SQL only)
|
||||
if self.can_access("schema_access", datasource.schema_perm or ""):
|
||||
return True
|
||||
|
||||
# Non-SQL explorables don't have schema hierarchy
|
||||
return False
|
||||
|
||||
def can_access_datasource(self, datasource: "BaseDatasource") -> bool:
|
||||
"""
|
||||
@@ -604,7 +624,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
self,
|
||||
form_data: dict[str, Any],
|
||||
dashboard: "Dashboard",
|
||||
datasource: "BaseDatasource",
|
||||
datasource: "BaseDatasource | Explorable",
|
||||
) -> bool:
|
||||
"""
|
||||
Return True if the form_data is performing a supported drill by operation,
|
||||
@@ -612,10 +632,10 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
|
||||
:param form_data: The form_data included in the request.
|
||||
:param dashboard: The dashboard the user is drilling from.
|
||||
:returns: Whether the user has drill byaccess.
|
||||
:param datasource: The datasource being queried
|
||||
:returns: Whether the user has drill by access.
|
||||
"""
|
||||
|
||||
from superset.connectors.sqla.models import TableColumn
|
||||
from superset.models.slice import Slice
|
||||
|
||||
return bool(
|
||||
@@ -630,16 +650,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
and slc in dashboard.slices
|
||||
and slc.datasource == datasource
|
||||
and (dimensions := form_data.get("groupby"))
|
||||
and (
|
||||
drillable_columns := {
|
||||
row[0]
|
||||
for row in self.session.query(TableColumn.column_name)
|
||||
.filter(TableColumn.table_id == datasource.id)
|
||||
.filter(TableColumn.groupby)
|
||||
.all()
|
||||
}
|
||||
)
|
||||
and set(dimensions).issubset(drillable_columns)
|
||||
and datasource.has_drill_by_columns(dimensions)
|
||||
)
|
||||
|
||||
def can_access_dashboard(self, dashboard: "Dashboard") -> bool:
|
||||
@@ -705,7 +716,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_datasource_access_error_msg(datasource: "BaseDatasource") -> str:
|
||||
def get_datasource_access_error_msg(
|
||||
datasource: "BaseDatasource | Explorable",
|
||||
) -> str:
|
||||
"""
|
||||
Return the error message for the denied Superset datasource.
|
||||
|
||||
@@ -714,13 +727,13 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
|
||||
return (
|
||||
f"This endpoint requires the datasource {datasource.id}, "
|
||||
f"This endpoint requires the datasource {datasource.data['id']}, "
|
||||
"database or `all_datasource_access` permission"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_datasource_access_link( # pylint: disable=unused-argument
|
||||
datasource: "BaseDatasource",
|
||||
datasource: "BaseDatasource | Explorable",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Return the link for the denied Superset datasource.
|
||||
@@ -732,7 +745,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
return get_conf().get("PERMISSION_INSTRUCTIONS_LINK")
|
||||
|
||||
def get_datasource_access_error_object( # pylint: disable=invalid-name
|
||||
self, datasource: "BaseDatasource"
|
||||
self, datasource: "BaseDatasource | Explorable"
|
||||
) -> SupersetError:
|
||||
"""
|
||||
Return the error object for the denied Superset datasource.
|
||||
@@ -746,8 +759,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
level=ErrorLevel.WARNING,
|
||||
extra={
|
||||
"link": self.get_datasource_access_link(datasource),
|
||||
"datasource": datasource.id,
|
||||
"datasource_name": datasource.name,
|
||||
"datasource": datasource.data["id"],
|
||||
"datasource_name": datasource.data["name"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2280,8 +2293,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
dashboard: Optional["Dashboard"] = None,
|
||||
chart: Optional["Slice"] = None,
|
||||
database: Optional["Database"] = None,
|
||||
datasource: Optional["BaseDatasource"] = None,
|
||||
query: Optional["Query"] = None,
|
||||
datasource: Optional["BaseDatasource | Explorable"] = None,
|
||||
query: Optional["Query | Explorable"] = None,
|
||||
query_context: Optional["QueryContext"] = None,
|
||||
table: Optional["Table"] = None,
|
||||
viz: Optional["BaseViz"] = None,
|
||||
@@ -2326,7 +2339,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
|
||||
if database and table or query:
|
||||
if query:
|
||||
database = query.database
|
||||
# Type narrow: only SQL Lab Query objects have .database attribute
|
||||
if hasattr(query, "database"):
|
||||
database = query.database
|
||||
|
||||
database = cast("Database", database)
|
||||
default_catalog = database.get_default_catalog()
|
||||
@@ -2334,7 +2349,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
if self.can_access_database(database):
|
||||
return
|
||||
|
||||
if query:
|
||||
# Type narrow: this path only applies to SQL Lab Query objects
|
||||
if query and hasattr(query, "sql") and hasattr(query, "catalog"):
|
||||
# Getting the default schema for a query is hard. Users can select the
|
||||
# schema in SQL Lab, but there's no guarantee that the query actually
|
||||
# will run in that schema. Each DB engine spec needs to implement the
|
||||
@@ -2342,8 +2358,11 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
# If the DB engine spec doesn't implement the logic the schema is read
|
||||
# from the SQLAlchemy URI if possible; if not, we use the SQLAlchemy
|
||||
# inspector to read it.
|
||||
from superset.models.sql_lab import Query
|
||||
|
||||
default_schema = database.get_default_schema_for_query(
|
||||
query, template_params
|
||||
cast(Query, query),
|
||||
template_params,
|
||||
)
|
||||
tables = {
|
||||
table_.qualify(
|
||||
@@ -2455,7 +2474,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
and dashboard_.json_metadata
|
||||
and (json_metadata := json.loads(dashboard_.json_metadata))
|
||||
and any(
|
||||
target.get("datasetId") == datasource.id
|
||||
target.get("datasetId") == datasource.data["id"]
|
||||
for fltr in json_metadata.get(
|
||||
"native_filter_configuration",
|
||||
[],
|
||||
@@ -2560,7 +2579,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
return super().get_user_roles(user)
|
||||
|
||||
def get_guest_rls_filters(
|
||||
self, dataset: "BaseDatasource"
|
||||
self, dataset: "BaseDatasource | Explorable"
|
||||
) -> list[GuestTokenRlsRule]:
|
||||
"""
|
||||
Retrieves the row level security filters for the current user and the dataset,
|
||||
@@ -2573,11 +2592,11 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
rule
|
||||
for rule in guest_user.rls
|
||||
if not rule.get("dataset")
|
||||
or str(rule.get("dataset")) == str(dataset.id)
|
||||
or str(rule.get("dataset")) == str(dataset.data["id"])
|
||||
]
|
||||
return []
|
||||
|
||||
def get_rls_filters(self, table: "BaseDatasource") -> list[SqlaQuery]:
|
||||
def get_rls_filters(self, table: "BaseDatasource | Explorable") -> list[SqlaQuery]:
|
||||
"""
|
||||
Retrieves the appropriate row level security filters for the current user and
|
||||
the passed table.
|
||||
@@ -2614,7 +2633,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
.filter(RLSFilterRoles.c.role_id.in_(user_roles))
|
||||
)
|
||||
filter_tables = self.session.query(RLSFilterTables.c.rls_filter_id).filter(
|
||||
RLSFilterTables.c.table_id == table.id
|
||||
RLSFilterTables.c.table_id == table.data["id"]
|
||||
)
|
||||
query = (
|
||||
self.session.query(
|
||||
@@ -2640,7 +2659,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
)
|
||||
return query.all()
|
||||
|
||||
def get_rls_sorted(self, table: "BaseDatasource") -> list["RowLevelSecurityFilter"]:
|
||||
def get_rls_sorted(
|
||||
self, table: "BaseDatasource | Explorable"
|
||||
) -> list["RowLevelSecurityFilter"]:
|
||||
"""
|
||||
Retrieves a list RLS filters sorted by ID for
|
||||
the current user and the passed table.
|
||||
@@ -2652,10 +2673,12 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
filters.sort(key=lambda f: f.id)
|
||||
return filters
|
||||
|
||||
def get_guest_rls_filters_str(self, table: "BaseDatasource") -> list[str]:
|
||||
def get_guest_rls_filters_str(
|
||||
self, table: "BaseDatasource | Explorable"
|
||||
) -> list[str]:
|
||||
return [f.get("clause", "") for f in self.get_guest_rls_filters(table)]
|
||||
|
||||
def get_rls_cache_key(self, datasource: "BaseDatasource") -> list[str]:
|
||||
def get_rls_cache_key(self, datasource: "Explorable | BaseDatasource") -> list[str]:
|
||||
rls_clauses_with_group_key = []
|
||||
if datasource.is_rls_supported:
|
||||
rls_clauses_with_group_key = [
|
||||
|
||||
Reference in New Issue
Block a user