豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 4b750c0

Browse files
betodealmeidamichael-s-molina
authored andcommitted
fix: guest queries (#27566)
(cherry picked from commit 36290ce)
1 parent f52647f commit 4b750c0

File tree

2 files changed

+132
-11
lines changed

2 files changed

+132
-11
lines changed

superset/security/manager.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
GuestTokenUser,
6969
GuestUser,
7070
)
71+
from superset.superset_typing import Metric
7172
from superset.utils.core import (
7273
DatasourceName,
7374
DatasourceType,
@@ -144,6 +145,13 @@ def __init__(self, **kwargs: Any) -> None:
144145
RoleModelView.related_views = []
145146

146147

148+
def freeze_metric(metric: Metric) -> str:
149+
"""
150+
Used to compare metric sets.
151+
"""
152+
return json.dumps(metric, sort_keys=True)
153+
154+
147155
def query_context_modified(query_context: "QueryContext") -> bool:
148156
"""
149157
Check if a query context has been modified.
@@ -154,29 +162,28 @@ def query_context_modified(query_context: "QueryContext") -> bool:
154162
form_data = query_context.form_data
155163
stored_chart = query_context.slice_
156164

157-
# sanity checks
165+
# native filter requests
158166
if form_data is None or stored_chart is None:
159-
return True
167+
return False
160168

161169
# cannot request a different chart
162170
if form_data.get("slice_id") != stored_chart.id:
163171
return True
164172

165173
# compare form_data
166174
requested_metrics = {
167-
frozenset(metric.items()) if isinstance(metric, dict) else metric
168-
for metric in form_data.get("metrics") or []
175+
freeze_metric(metric) for metric in form_data.get("metrics") or []
169176
}
170177
stored_metrics = {
171-
frozenset(metric.items()) if isinstance(metric, dict) else metric
178+
freeze_metric(metric)
172179
for metric in stored_chart.params_dict.get("metrics") or []
173180
}
174181
if not requested_metrics.issubset(stored_metrics):
175182
return True
176183

177184
# compare queries in query_context
178185
queries_metrics = {
179-
frozenset(metric.items()) if isinstance(metric, dict) else metric
186+
freeze_metric(metric)
180187
for query in query_context.queries
181188
for metric in query.metrics or []
182189
}
@@ -185,10 +192,7 @@ def query_context_modified(query_context: "QueryContext") -> bool:
185192
stored_query_context = json.loads(cast(str, stored_chart.query_context))
186193
for query in stored_query_context.get("queries") or []:
187194
stored_metrics.update(
188-
{
189-
frozenset(metric.items()) if isinstance(metric, dict) else metric
190-
for metric in query.get("metrics") or []
191-
}
195+
{freeze_metric(metric) for metric in query.get("metrics") or []}
192196
)
193197

194198
return not queries_metrics.issubset(stored_metrics)

tests/unit_tests/security/manager_test.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from superset.exceptions import SupersetSecurityException
2727
from superset.extensions import appbuilder
2828
from superset.models.slice import Slice
29-
from superset.security.manager import SupersetSecurityManager
29+
from superset.security.manager import query_context_modified, SupersetSecurityManager
3030
from superset.sql_parse import Table
3131
from superset.superset_typing import AdhocMetric
3232
from superset.utils.core import override_user
@@ -414,3 +414,120 @@ def test_raise_for_access_chart_owner(
414414
sm.raise_for_access(
415415
chart=slice,
416416
)
417+
418+
419+
def test_query_context_modified(
420+
mocker: MockFixture,
421+
stored_metrics: list[AdhocMetric],
422+
) -> None:
423+
"""
424+
Test the `query_context_modified` function.
425+
426+
The function is used to ensure guest users are not modifying the request payload on
427+
embedded dashboard, preventing users from modifying it to access metrics different
428+
from the ones stored in dashboard charts.
429+
"""
430+
query_context = mocker.MagicMock()
431+
query_context.slice_.id = 42
432+
query_context.slice_.query_context = None
433+
query_context.slice_.params_dict = {
434+
"metrics": stored_metrics,
435+
}
436+
437+
query_context.form_data = {
438+
"slice_id": 42,
439+
"metrics": stored_metrics,
440+
}
441+
query_context.queries = [QueryObject(metrics=stored_metrics)] # type: ignore
442+
assert not query_context_modified(query_context)
443+
444+
445+
def test_query_context_modified_tampered(
446+
mocker: MockFixture,
447+
stored_metrics: list[AdhocMetric],
448+
) -> None:
449+
"""
450+
Test the `query_context_modified` function when the request is tampered with.
451+
452+
The function is used to ensure guest users are not modifying the request payload on
453+
embedded dashboard, preventing users from modifying it to access metrics different
454+
from the ones stored in dashboard charts.
455+
"""
456+
query_context = mocker.MagicMock()
457+
query_context.slice_.id = 42
458+
query_context.slice_.query_context = None
459+
query_context.slice_.params_dict = {
460+
"metrics": stored_metrics,
461+
}
462+
463+
tampered_metrics = [
464+
{
465+
"column": None,
466+
"expressionType": "SQL",
467+
"hasCustomLabel": False,
468+
"label": "COUNT(*) + 2",
469+
"sqlExpression": "COUNT(*) + 2",
470+
}
471+
]
472+
473+
query_context.form_data = {
474+
"slice_id": 42,
475+
"metrics": tampered_metrics,
476+
}
477+
query_context.queries = [QueryObject(metrics=tampered_metrics)] # type: ignore
478+
assert query_context_modified(query_context)
479+
480+
481+
def test_query_context_modified_native_filter(mocker: MockFixture) -> None:
482+
"""
483+
Test the `query_context_modified` function with a native filter request.
484+
485+
A native filter request has no chart (slice) associated with it.
486+
"""
487+
query_context = mocker.MagicMock()
488+
query_context.slice_ = None
489+
490+
assert not query_context_modified(query_context)
491+
492+
493+
def test_query_context_modified_mixed_chart(mocker: MockFixture) -> None:
494+
"""
495+
Test the `query_context_modified` function for a mixed chart request.
496+
497+
The metrics in the mixed chart are a nested dictionary (due to `columns`), and need
498+
to be serialized to JSON with the keys sorted in order to compare the request
499+
metrics with the chart metrics.
500+
"""
501+
stored_metrics = [
502+
{
503+
"optionName": "metric_vgops097wej_g8uff99zhk7",
504+
"label": "AVG(num)",
505+
"expressionType": "SIMPLE",
506+
"column": {"column_name": "num", "type": "BIGINT(20)"},
507+
"aggregate": "AVG",
508+
}
509+
]
510+
# different order (remember, dicts have order!)
511+
requested_metrics = [
512+
{
513+
"aggregate": "AVG",
514+
"column": {"column_name": "num", "type": "BIGINT(20)"},
515+
"expressionType": "SIMPLE",
516+
"label": "AVG(num)",
517+
"optionName": "metric_vgops097wej_g8uff99zhk7",
518+
}
519+
]
520+
521+
query_context = mocker.MagicMock()
522+
query_context.slice_.id = 42
523+
query_context.slice_.query_context = None
524+
query_context.slice_.params_dict = {
525+
"metrics": stored_metrics,
526+
}
527+
528+
query_context.form_data = {
529+
"slice_id": 42,
530+
"metrics": requested_metrics,
531+
}
532+
query_context.queries = [QueryObject(metrics=requested_metrics)] # type: ignore
533+
assert not query_context_modified(query_context)

0 commit comments

Comments
 (0)