mirror of
https://github.com/apache/superset.git
synced 2026-06-28 19:05:31 +00:00
Compare commits
3 Commits
feat/csp-r
...
fix-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57b93055b6 | ||
|
|
dc64716c61 | ||
|
|
6f12dbf0e1 |
@@ -472,6 +472,7 @@ export function transformFormulaAnnotation(
|
||||
return {
|
||||
name,
|
||||
id: name,
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: color || colorScale(name, sliceId),
|
||||
},
|
||||
@@ -565,6 +566,7 @@ export function transformIntervalAnnotation(
|
||||
id: `Interval - ${name}`,
|
||||
type: 'line',
|
||||
animation: false,
|
||||
z: 10,
|
||||
markArea: {
|
||||
silent: false,
|
||||
itemStyle: {
|
||||
@@ -660,6 +662,7 @@ export function transformEventAnnotation(
|
||||
id: `Event - ${name}`,
|
||||
type: 'line',
|
||||
animation: false,
|
||||
z: 10,
|
||||
markLine: {
|
||||
silent: false,
|
||||
symbol: 'none',
|
||||
@@ -705,6 +708,7 @@ export function transformTimeseriesAnnotation(
|
||||
type: 'line',
|
||||
id: name,
|
||||
name,
|
||||
z: 10,
|
||||
data,
|
||||
symbolSize: showMarkers ? markerSize : 0,
|
||||
itemStyle: computedStyle,
|
||||
|
||||
@@ -122,4 +122,8 @@ export const TOOLTIP_POINTER_MARGIN = 10;
|
||||
// from the edge of the window should the tooltip be kept
|
||||
export const TOOLTIP_OVERFLOW_MARGIN = 5;
|
||||
|
||||
// Minimum distance from the top of the chart container to keep the tooltip,
|
||||
// reserving space for annotation labels rendered at insideEndTop of markLines/markAreas
|
||||
export const TOOLTIP_TOP_CLEARANCE = 40;
|
||||
|
||||
export const DEFAULT_LOCALE = 'en';
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
} from '@superset-ui/core';
|
||||
import { TOOLTIP_OVERFLOW_MARGIN, TOOLTIP_POINTER_MARGIN } from '../constants';
|
||||
import {
|
||||
TOOLTIP_OVERFLOW_MARGIN,
|
||||
TOOLTIP_POINTER_MARGIN,
|
||||
TOOLTIP_TOP_CLEARANCE,
|
||||
} from '../constants';
|
||||
import { Refs } from '../types';
|
||||
|
||||
export function getDefaultTooltip(refs: Refs) {
|
||||
@@ -107,18 +111,44 @@ export function getDefaultTooltip(refs: Refs) {
|
||||
}
|
||||
}
|
||||
|
||||
// Position tooltip above cursor, or below if no space
|
||||
yPos = mouseY - TOOLTIP_POINTER_MARGIN - effectiveTooltipHeight;
|
||||
// Mirror horizontal logic: position tooltip below cursor when in top half of chart,
|
||||
// above cursor when in bottom half. This prevents the tooltip from covering annotation
|
||||
// labels that appear at the top of the chart (markArea/markLine labels).
|
||||
const chartHeight = divRect?.height || viewportHeight;
|
||||
const cursorYInChart = canvasMousePos[1];
|
||||
const isInTopHalfOfChart = cursorYInChart < chartHeight / 2;
|
||||
|
||||
// The tooltip is overflowing past the top edge of the window
|
||||
if (yPos <= 0) {
|
||||
// Attempt to place the tooltip to the bottom of the mouse position
|
||||
if (isInTopHalfOfChart) {
|
||||
yPos = mouseY + TOOLTIP_POINTER_MARGIN;
|
||||
|
||||
// The tooltip is overflowing past the bottom edge of the window
|
||||
if (yPos + effectiveTooltipHeight >= viewportHeight)
|
||||
// Place the tooltip a fixed distance from the top edge of the window
|
||||
yPos = TOOLTIP_OVERFLOW_MARGIN;
|
||||
if (yPos + effectiveTooltipHeight >= viewportHeight) {
|
||||
yPos = mouseY - TOOLTIP_POINTER_MARGIN - effectiveTooltipHeight;
|
||||
|
||||
if (yPos <= 0) {
|
||||
yPos = TOOLTIP_OVERFLOW_MARGIN;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yPos = mouseY - TOOLTIP_POINTER_MARGIN - effectiveTooltipHeight;
|
||||
|
||||
if (yPos <= 0) {
|
||||
yPos = mouseY + TOOLTIP_POINTER_MARGIN;
|
||||
|
||||
if (yPos + effectiveTooltipHeight >= viewportHeight) {
|
||||
yPos = TOOLTIP_OVERFLOW_MARGIN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp tooltip away from the top of the chart to avoid covering annotation labels
|
||||
// (markLine/markArea labels rendered at insideEndTop are within the first ~40px)
|
||||
if (divRect) {
|
||||
yPos = Math.max(yPos, divRect.y + TOOLTIP_TOP_CLEARANCE);
|
||||
// Re-apply bottom overflow check in case top clearance pushed the tooltip off-screen
|
||||
if (yPos + effectiveTooltipHeight >= viewportHeight) {
|
||||
yPos =
|
||||
viewportHeight - effectiveTooltipHeight - TOOLTIP_OVERFLOW_MARGIN;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the position (converted back to a relative position on the canvas)
|
||||
|
||||
@@ -227,7 +227,10 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
formData?.result_type === 'results');
|
||||
|
||||
if (isDownloadQuery) {
|
||||
moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
|
||||
moreProps.row_limit =
|
||||
formDataCopy.row_limit != null
|
||||
? Number(formDataCopy.row_limit)
|
||||
: undefined;
|
||||
moreProps.row_offset = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -562,7 +562,6 @@ const Chart = (props: ChartProps) => {
|
||||
exportFormData as unknown as import('@superset-ui/core').QueryFormData,
|
||||
resultType,
|
||||
resultFormat: format,
|
||||
force: true,
|
||||
ownState: exportOwnState,
|
||||
onStartStreamingExport: shouldUseStreaming
|
||||
? (exportParams: JsonObject) => {
|
||||
|
||||
@@ -223,6 +223,29 @@ class BaseSupersetApiMixin:
|
||||
f"{self.__class__.__name__}.{func_name}.{action}"
|
||||
)
|
||||
|
||||
def log_rejected_field_access(self, func_name: str, column_name: str) -> None:
|
||||
"""Emit a security log event when a related/distinct field is rejected.
|
||||
|
||||
The allowlist check itself blocks the request; this records the attempt
|
||||
in the structured log (alongside the existing statsd counter) so that
|
||||
rejected field access is visible to security monitoring and forensics,
|
||||
with the caller's identity, the endpoint, and the attempted value.
|
||||
"""
|
||||
# Sanitize the user-supplied column name to a single, bounded token so
|
||||
# it cannot inject newlines or forge extra key=value tokens in the log
|
||||
# line. Restrict to a safe character set (column names are alphanumeric
|
||||
# plus ``_-.``) and replace anything else with ``?``.
|
||||
sanitized_column = "".join(
|
||||
ch if (ch.isalnum() or ch in "_-.") else "?" for ch in str(column_name)
|
||||
)[:200]
|
||||
logger.warning(
|
||||
"Rejected disallowed field access: user_id=%s endpoint=%s.%s column=%s",
|
||||
get_user_id(),
|
||||
self.__class__.__name__,
|
||||
func_name,
|
||||
sanitized_column,
|
||||
)
|
||||
|
||||
def timing_stats(self, action: str, func_name: str, value: float) -> None:
|
||||
"""
|
||||
Proxy function for statsd.incr to impose a key structure for REST API's
|
||||
@@ -619,6 +642,7 @@ class BaseSupersetModelRestApi(BaseSupersetApiMixin, ModelRestApi):
|
||||
return response
|
||||
if column_name not in self.allowed_rel_fields:
|
||||
self.incr_stats("error", self.related.__name__)
|
||||
self.log_rejected_field_access(self.related.__name__, column_name)
|
||||
return self.response_404()
|
||||
args = kwargs.get("rison", {})
|
||||
|
||||
@@ -697,7 +721,8 @@ class BaseSupersetModelRestApi(BaseSupersetApiMixin, ModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
if column_name not in self.allowed_distinct_fields:
|
||||
self.incr_stats("error", self.related.__name__)
|
||||
self.incr_stats("error", self.distinct.__name__)
|
||||
self.log_rejected_field_access(self.distinct.__name__, column_name)
|
||||
return self.response_404()
|
||||
args = kwargs.get("rison", {})
|
||||
# handle pagination
|
||||
|
||||
@@ -423,5 +423,13 @@ class ApiOwnersTestCaseMixin:
|
||||
self.login(ADMIN_USERNAME)
|
||||
uri = f"api/v1/{self.resource_name}/related/owner"
|
||||
|
||||
rv = self.client.get(uri)
|
||||
with patch("superset.views.base_api.logger") as mock_logger:
|
||||
rv = self.client.get(uri)
|
||||
assert rv.status_code == 404
|
||||
# A disallowed related field is recorded as a security log event,
|
||||
# including the rejected column name, in addition to the 404.
|
||||
mock_logger.warning.assert_called_once()
|
||||
assert "column=owner" in (
|
||||
mock_logger.warning.call_args.args[0]
|
||||
% mock_logger.warning.call_args.args[1:]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user