feat: Enable drilling in embedded (#34319)

This commit is contained in:
Vitor Avila
2025-08-05 02:23:00 -03:00
committed by GitHub
parent 791ea9860d
commit 49689eec6c
29 changed files with 1510 additions and 336 deletions

View File

@@ -84,16 +84,21 @@ class TestDatasetApi(SupersetTestCase):
def insert_dataset(
table_name: str,
owners: list[int],
database: Database,
database: Database | None = None,
sql: str | None = None,
schema: str | None = None,
catalog: str | None = None,
fetch_metadata: bool = True,
columns: list[TableColumn] | None = None,
metrics: list[SqlMetric] | None = None,
extra: str | None = None,
) -> SqlaTable:
obj_owners = list() # noqa: C408
for owner in owners:
user = db.session.query(security_manager.user_model).get(owner)
obj_owners.append(user)
database = database or get_example_database()
schema = schema or get_example_default_schema()
table = SqlaTable(
table_name=table_name,
schema=schema,
@@ -101,13 +106,36 @@ class TestDatasetApi(SupersetTestCase):
database=database,
sql=sql,
catalog=catalog,
extra=extra,
)
if columns:
table.columns = columns
if metrics:
table.metrics = metrics
db.session.add(table)
db.session.commit()
if fetch_metadata:
table.fetch_metadata()
return table
@staticmethod
def insert_chart(
chart_title: str,
dataset_id: int,
viz_type: str = "bar",
params: str = "{}",
) -> Slice:
chart = Slice(
slice_name=chart_title,
datasource_id=dataset_id,
datasource_type="table",
viz_type=viz_type,
params=params,
)
db.session.add(chart)
db.session.commit()
return chart
def insert_default_dataset(self):
return self.insert_dataset(
"ab_permission", [self.get_user("admin").id], get_main_database()
@@ -421,12 +449,11 @@ class TestDatasetApi(SupersetTestCase):
"""
Dataset API: Test get dataset with the render parameter.
"""
database = get_example_database()
dataset = SqlaTable(
dataset = self.insert_dataset(
table_name="test_sql_table_with_jinja",
database=database,
schema=get_example_default_schema(),
main_dttm_col="default_dttm",
owners=[],
sql="SELECT {{ current_user_id() }} as my_user_id",
fetch_metadata=False,
columns=[
TableColumn(
column_name="my_user_id",
@@ -446,10 +473,7 @@ class TestDatasetApi(SupersetTestCase):
expression="{{ url_param('multiplier') }} * 1.4",
)
],
sql="SELECT {{ current_user_id() }} as my_user_id",
)
db.session.add(dataset)
db.session.commit()
self.login(ADMIN_USERNAME)
admin = self.get_user(ADMIN_USERNAME)
@@ -493,12 +517,11 @@ class TestDatasetApi(SupersetTestCase):
Dataset API: Test get dataset with the render parameter
when rendering raises an exception.
"""
database = get_example_database()
dataset = SqlaTable(
dataset = self.insert_dataset(
table_name="test_sql_table_with_incorrect_jinja",
database=database,
schema=get_example_default_schema(),
main_dttm_col="default_dttm",
owners=[],
sql="SELECT {{ current_user_id() } as my_user_id",
fetch_metadata=False,
columns=[
TableColumn(
column_name="my_user_id",
@@ -518,10 +541,7 @@ class TestDatasetApi(SupersetTestCase):
expression="{{ url_param('multiplier') } * 1.4",
)
],
sql="SELECT {{ current_user_id() } as my_user_id",
)
db.session.add(dataset)
db.session.commit()
self.login(ADMIN_USERNAME)
@@ -679,6 +699,7 @@ class TestDatasetApi(SupersetTestCase):
"can_duplicate",
"can_get_or_create_dataset",
"can_warm_up_cache",
"can_get_drill_info",
}
def test_create_dataset_item(self):
@@ -2716,17 +2737,12 @@ class TestDatasetApi(SupersetTestCase):
"""
Dataset API: Test custom dataset_is_certified filter
"""
table_w_certification = SqlaTable(
table_w_certification = self.insert_dataset(
table_name="foo",
schema=None,
owners=[],
database=get_main_database(),
sql=None,
fetch_metadata=False,
extra='{"certification": 1}',
)
db.session.add(table_w_certification)
db.session.commit()
arguments = {
"filters": [{"col": "id", "opr": "dataset_is_certified", "value": True}]
@@ -2999,3 +3015,478 @@ class TestDatasetApi(SupersetTestCase):
assert data == {
"message": "The provided table was not found in the provided database"
}
def test_get_drill_info_admin_user(self):
"""
Dataset API: Test drill_info endpoint returns metadata for admin users, even
without a dashboard param.
"""
self.login(ADMIN_USERNAME)
dataset = self.insert_dataset(
table_name="test_drill_dataset",
owners=[],
columns=[
TableColumn(
column_name="category",
type="VARCHAR(255)",
verbose_name="Category Column",
groupby=True,
),
TableColumn(
column_name="region",
type="VARCHAR(255)",
groupby=True,
),
TableColumn(
column_name="value",
type="VARCHAR(255)",
groupby=False,
),
TableColumn(
column_name="description",
type="VARCHAR(255)",
groupby=False,
),
],
fetch_metadata=False,
)
# Test the drill_info endpoint
uri = f"api/v1/dataset/{dataset.id}/drill_info/"
rv = self.get_assert_metric(uri, "get_drill_info")
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
result = data["result"]
# Verify admin gets full dataset metadata
assert "created_by" in result
assert "created_on_humanized" in result
assert "changed_by" in result
assert "changed_on_humanized" in result
assert result["id"] == dataset.id
assert result["table_name"] == "test_drill_dataset"
assert result["owners"] == []
assert len(result["columns"]) == 2
assert result["columns"] == [
{"column_name": "category", "verbose_name": "Category Column"},
{"column_name": "region", "verbose_name": None},
]
self.items_to_delete = [dataset]
def test_get_drill_info_admin_user_dataset_not_found(self):
"""
Dataset API: Test drill_info endpoint returns 404 for non-existent dataset.
"""
self.login(ADMIN_USERNAME)
uri = "api/v1/dataset/99999/drill_info/"
rv = self.client.get(uri)
assert rv.status_code == 404
def test_get_drill_info_no_perm_to_drill(self):
"""
Dataset API: Test drill_info endpoint returns 403 for users without permission
to access the API.
"""
dataset = self.insert_dataset(table_name="foo", owners=[], fetch_metadata=False)
# Log in as alpha for dataset access but remove pvm access
with self.temporary_user(
clone_user=security_manager.find_user(username=ALPHA_USERNAME),
pvms_to_remove=[("can_get_drill_info", "Dataset")],
login=True,
):
uri = f"api/v1/dataset/{dataset.id}/drill_info/"
rv = self.client.get(uri)
assert rv.status_code == 403
self.items_to_delete = [dataset]
@patch("superset.security.manager.SupersetSecurityManager.has_guest_access")
@patch("superset.security.manager.SupersetSecurityManager.is_guest_user")
@with_feature_flags(EMBEDDED_SUPERSET=True)
def test_get_drill_info_embedded_user_no_perm_to_drill(
self, mock_is_guest_user, mock_has_guest_access
):
"""
Dataset API: Test drill_info endpoint returns 403 for embedded users when
the role does not have permission.
"""
dataset = self.insert_dataset(
table_name="test_embedded_dataset",
owners=[],
columns=[
TableColumn(
column_name="category",
type="VARCHAR(255)",
verbose_name="Category Column",
groupby=True,
),
TableColumn(
column_name="region",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test Embedded Chart", dataset.id)
dash = self.insert_dashboard(
"Embedded Test Dashboard", "embedded-test-dashboard", [], slices=[chart]
)
# Log in to role without `can_get_drill_info` permission, and mock guest checks
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
pvms_to_remove=[("can_get_drill_info", "Dataset")],
login=True,
):
mock_is_guest_user.return_value = True
mock_has_guest_access.return_value = True
uri = f"api/v1/dataset/{dataset.id}/drill_info/?q=(dashboard_id:{dash.id})"
rv = self.client.get(uri)
assert rv.status_code == 403
self.items_to_delete = [dash, chart, dataset]
@patch("superset.security.manager.SupersetSecurityManager.has_guest_access")
@patch("superset.security.manager.SupersetSecurityManager.is_guest_user")
@with_feature_flags(EMBEDDED_SUPERSET=True)
def test_get_drill_info_embedded_user_with_dashboard_id(
self, mock_is_guest_user, mock_has_guest_access
):
"""
Dataset API: Test drill_info endpoint with dashboard ID parameter for
embedded users.
"""
dataset = self.insert_dataset(
table_name="test_embedded_dataset",
owners=[],
columns=[
TableColumn(
column_name="category",
type="VARCHAR(255)",
verbose_name="Category Column",
groupby=True,
),
TableColumn(
column_name="region",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test Embedded Chart", dataset.id)
dash = self.insert_dashboard(
"Embedded Test Dashboard", "embedded-test-dashboard", [], slices=[chart]
)
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
login=True,
):
mock_is_guest_user.return_value = True
mock_has_guest_access.return_value = True
uri = f"api/v1/dataset/{dataset.id}/drill_info/?q=(dashboard_id:{dash.id})"
rv = self.client.get(uri)
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
result = data["result"]
assert result == {
"id": dataset.id,
"columns": [
{"column_name": "category", "verbose_name": "Category Column"},
{"column_name": "region", "verbose_name": None},
],
}
self.items_to_delete = [dash, chart, dataset]
@patch("superset.security.manager.SupersetSecurityManager.has_guest_access")
@patch("superset.security.manager.SupersetSecurityManager.is_guest_user")
@with_feature_flags(EMBEDDED_SUPERSET=True)
def test_get_drill_info_embedded_user_without_dashboard_parameter(
self, mock_is_guest_user, mock_has_guest_access
):
"""
Dataset API: Test drill_info endpoint without dashboard ID parameter
for embedded users.
"""
dataset = self.insert_dataset(
table_name="test_embedded_dataset",
owners=[],
columns=[
TableColumn(
column_name="category",
type="VARCHAR(255)",
verbose_name="Category Column",
groupby=True,
),
TableColumn(
column_name="region",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test Embedded Chart", dataset.id)
dashboard = self.insert_dashboard(
"Embedded Test Dashboard", "embedded-test-dashboard", [], slices=[chart]
)
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
login=True,
):
mock_is_guest_user.return_value = True
mock_has_guest_access.return_value = True
uri = f"api/v1/dataset/{dataset.id}/drill_info/"
rv = self.client.get(uri)
assert rv.status_code == 403
self.items_to_delete = [dashboard, chart, dataset]
@patch("superset.security.manager.SupersetSecurityManager.has_guest_access")
@patch("superset.security.manager.SupersetSecurityManager.is_guest_user")
@with_feature_flags(EMBEDDED_SUPERSET=True)
def test_get_drill_info_embedded_user_dashboard_without_dataset(
self, mock_is_guest_user, mock_has_guest_access
):
"""
Dataset API: Test drill_info with dashboard ID that user has access to but
does not contain the dataset.
"""
dataset = self.insert_dataset(
table_name="test_d2d_table",
owners=[],
columns=[
TableColumn(
column_name="category",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
dashboard_dataset = self.insert_dataset(
table_name="test_dashboard_dataset",
owners=[],
fetch_metadata=False,
)
chart = self.insert_chart("Dashboard Chart", dashboard_dataset.id)
dash = self.insert_dashboard(
"Dashboard Without Test Dataset",
"dashboard-without-test-dataset",
[],
slices=[chart],
)
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
login=True,
):
mock_is_guest_user.return_value = True
mock_has_guest_access.return_value = True
uri = f"api/v1/dataset/{dataset.id}/drill_info/?q=(dashboard_id:{dash.id})"
rv = self.client.get(uri)
assert rv.status_code == 403
self.items_to_delete = [dash, chart, dataset, dashboard_dataset]
@with_feature_flags(DASHBOARD_RBAC=True)
def test_get_drill_info_dashboard_rbac_access_granted(self):
"""
Dataset API: Test drill_info with dashboard parameter when user has access
via the DASHBOARD_RBAC FF.
"""
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME)
) as test_user:
user_role_ids = [role.id for role in test_user.roles]
# Login as admin to avoid FK issues during temp account deletion
self.login(ADMIN_USERNAME)
dataset = self.insert_dataset(
table_name="test_rbac_dataset",
owners=[],
columns=[
TableColumn(
column_name="restricted_column",
type="VARCHAR(255)",
verbose_name="Restricted Column",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test RBAC Chart", dataset.id)
dash = self.insert_dashboard(
"RBAC Test Dashboard",
"rbac-test-dashboard",
[],
roles=user_role_ids,
slices=[chart],
published=True,
)
self.logout()
self.login(test_user.username)
uri = f"api/v1/dataset/{dataset.id}/drill_info/?q=(dashboard_id:{dash.id})"
rv = self.client.get(uri)
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
result = data["result"]
assert "created_by" in result
assert "created_on_humanized" in result
assert "changed_by" in result
assert "changed_on_humanized" in result
assert result["id"] == dataset.id
assert result["table_name"] == "test_rbac_dataset"
assert len(result["columns"]) == 1
assert result["columns"][0]["column_name"] == "restricted_column"
self.items_to_delete = [dash, chart, dataset]
@with_feature_flags(DASHBOARD_RBAC=True)
def test_get_drill_info_dashboard_rbac_no_perm_to_drill(self):
"""
Dataset API: Test drill_info with dashboard parameter when user has
no permission to access the API.
"""
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
pvms_to_remove=[("can_get_drill_info", "Dataset")],
) as test_user:
user_role_ids = [role.id for role in test_user.roles]
self.login(ADMIN_USERNAME)
dataset = self.insert_dataset(
table_name="test_rbac_dataset_denied",
owners=[],
columns=[
TableColumn(
column_name="restricted_column",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test RBAC Chart second", dataset.id)
dash = self.insert_dashboard(
"RBAC Test Dashboard 2",
"rbac-test-dashboard-2",
[],
slices=[chart],
roles=user_role_ids,
published=True,
)
self.logout()
self.login(test_user.username)
uri = f"api/v1/dataset/{dataset.id}/drill_info/?q=(dashboard_id:{dash.id})"
rv = self.client.get(uri)
assert rv.status_code == 403
self.items_to_delete = [dash, chart, dataset]
@with_feature_flags(DASHBOARD_RBAC=True)
def test_get_drill_info_dashboard_rbac_no_access_on_dashboard(self):
"""
Dataset API: Test drill_info with dashboard parameter when user has
no access to the dashboard.
"""
dataset = self.insert_dataset(
table_name="test_rbac_dataset_denied",
owners=[],
columns=[
TableColumn(
column_name="restricted_column",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test RBAC Chart second", dataset.id)
dash = self.insert_dashboard(
"RBAC Test Dashboard 2",
"rbac-test-dashboard-2",
[],
slices=[chart],
roles=[],
published=True,
)
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
login=True,
username="test_new_account",
):
uri = f"api/v1/dataset/{dataset.id}/drill_info/?q=(dashboard_id:{dash.id})"
rv = self.client.get(uri)
assert rv.status_code == 403
self.items_to_delete = [dash, chart, dataset]
@with_feature_flags(DASHBOARD_RBAC=True)
def test_get_drill_info_dashboard_rbac_no_dashboard_id(self):
"""
Dataset API: Test drill_info without dashboard ID parameter falls back
to regular access control.
"""
with self.temporary_user(
clone_user=security_manager.find_user(username=GAMMA_USERNAME),
) as test_user:
self.login(ADMIN_USERNAME)
user_role_ids = [role.id for role in test_user.roles]
dataset = self.insert_dataset(
table_name="test_no_dashboard_id",
owners=[],
columns=[
TableColumn(
column_name="restricted_column",
type="VARCHAR(255)",
groupby=True,
),
],
fetch_metadata=False,
)
chart = self.insert_chart("Test RBAC Chart second", dataset.id)
dashboard = self.insert_dashboard(
"RBAC Test Dashboard 2",
"rbac-test-dashboard-2",
[],
slices=[chart],
roles=user_role_ids,
published=True,
)
self.logout()
self.login(test_user.username)
uri = f"api/v1/dataset/{dataset.id}/drill_info/"
rv = self.client.get(uri)
assert rv.status_code == 404
self.items_to_delete = [dashboard, chart, dataset]