This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4f5789abfe fix(reports): Use authenticated user as recipient for 
chart/dashboard reports (#36981)
4f5789abfe is described below

commit 4f5789abfe88ca82672e4000ab2df3f7c36f9230
Author: Mehmet Salih Yavuz <[email protected]>
AuthorDate: Mon Jan 12 12:29:54 2026 +0300

    fix(reports): Use authenticated user as recipient for chart/dashboard 
reports (#36981)
---
 superset/commands/report/create.py                 |  35 ++++
 superset/commands/report/exceptions.py             |  15 ++
 superset/reports/schemas.py                        |   2 +-
 .../commands/report/test_create_recipients.py      | 179 +++++++++++++++++++++
 4 files changed, 230 insertions(+), 1 deletion(-)

diff --git a/superset/commands/report/create.py 
b/superset/commands/report/create.py
index e238102f90..b3cd0dd16f 100644
--- a/superset/commands/report/create.py
+++ b/superset/commands/report/create.py
@@ -18,6 +18,7 @@ import logging
 from functools import partial
 from typing import Any, Optional
 
+from flask import g
 from flask_babel import gettext as _
 from marshmallow import ValidationError
 
@@ -30,11 +31,13 @@ from superset.commands.report.exceptions import (
     ReportScheduleCreationMethodUniquenessValidationError,
     ReportScheduleInvalidError,
     ReportScheduleNameUniquenessValidationError,
+    ReportScheduleUserEmailNotFoundError,
 )
 from superset.daos.database import DatabaseDAO
 from superset.daos.report import ReportScheduleDAO
 from superset.reports.models import (
     ReportCreationMethod,
+    ReportRecipientType,
     ReportSchedule,
     ReportScheduleType,
 )
@@ -54,6 +57,35 @@ class CreateReportScheduleCommand(CreateMixin, 
BaseReportScheduleCommand):
         self.validate()
         return ReportScheduleDAO.create(attributes=self._properties)
 
+    def _populate_recipients(self, exceptions: list[ValidationError]) -> None:
+        """
+        Populate recipients based on creation method and current user.
+
+        For reports initiated from charts or dashboards, always use
+        the current user's email as the recipient, ignoring any
+        client-provided recipient values. Raises validation error if
+        user has no email address.
+        """
+        creation_method = self._properties.get("creation_method")
+
+        # For reports from charts/dashboards, always use current user
+        if creation_method in (
+            ReportCreationMethod.CHARTS,
+            ReportCreationMethod.DASHBOARDS,
+        ):
+            if hasattr(g, "user") and g.user and g.user.email:
+                # Override any provided recipients with current user's email
+                self._properties["recipients"] = [
+                    {
+                        "type": ReportRecipientType.EMAIL,
+                        "recipient_config_json": {"target": g.user.email},
+                    }
+                ]
+            else:
+                # User doesn't have an email address - can't create report
+                exceptions.append(ReportScheduleUserEmailNotFoundError())
+        # For creation from alerts_reports view, keep the recipients as 
provided
+
     def validate(self) -> None:
         """
         Validates the properties of a report schedule configuration, including 
uniqueness
@@ -75,6 +107,9 @@ class CreateReportScheduleCommand(CreateMixin, 
BaseReportScheduleCommand):
 
         exceptions: list[ValidationError] = []
 
+        # Populate recipients if needed (may add validation errors)
+        self._populate_recipients(exceptions)
+
         # Validate name type uniqueness
         if not ReportScheduleDAO.validate_update_uniqueness(name, report_type):
             exceptions.append(
diff --git a/superset/commands/report/exceptions.py 
b/superset/commands/report/exceptions.py
index 8688627810..27966e6f09 100644
--- a/superset/commands/report/exceptions.py
+++ b/superset/commands/report/exceptions.py
@@ -309,3 +309,18 @@ class ReportScheduleForbiddenError(ForbiddenError):
 
 class ReportSchedulePruneLogError(CommandException):
     message = _("An error occurred while pruning logs ")
+
+
+class ReportScheduleUserEmailNotFoundError(ValidationError):
+    """
+    Validation error when user email is required but not found
+    """
+
+    def __init__(self) -> None:
+        super().__init__(
+            _(
+                "Unable to create report: User email address is required but 
not "
+                "found. Please ensure your user profile has a valid email 
address."
+            ),
+            field_name="recipients",
+        )
diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py
index cfccc579bc..76108f976f 100644
--- a/superset/reports/schemas.py
+++ b/superset/reports/schemas.py
@@ -224,7 +224,7 @@ class ReportSchedulePostSchema(Schema):
         validate=[Range(min=1, error=_("Value must be greater than 0"))],
     )
 
-    recipients = fields.List(fields.Nested(ReportRecipientSchema))
+    recipients = fields.List(fields.Nested(ReportRecipientSchema), 
required=False)
     report_format = fields.String(
         dump_default=ReportDataFormat.PNG,
         validate=validate.OneOf(choices=tuple(key.value for key in 
ReportDataFormat)),
diff --git a/tests/unit_tests/commands/report/test_create_recipients.py 
b/tests/unit_tests/commands/report/test_create_recipients.py
new file mode 100644
index 0000000000..3956302e04
--- /dev/null
+++ b/tests/unit_tests/commands/report/test_create_recipients.py
@@ -0,0 +1,179 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from unittest.mock import MagicMock, patch
+
+from superset.commands.report.create import CreateReportScheduleCommand
+from superset.commands.report.exceptions import 
ReportScheduleUserEmailNotFoundError
+from superset.reports.models import (
+    ReportCreationMethod,
+    ReportRecipientType,
+)
+
+
+def test_populate_recipients_chart_creation_with_user_email() -> None:
+    """Test that chart/dashboard creation methods use current user's email."""
+    with patch("superset.commands.report.create.g") as mock_g:
+        # Setup user with email
+        mock_user = MagicMock()
+        mock_user.email = "[email protected]"
+        mock_g.user = mock_user
+
+        # Create command instance
+        command = CreateReportScheduleCommand({})
+        command._properties = {
+            "creation_method": ReportCreationMethod.CHARTS,
+            "recipients": [
+                {
+                    "type": ReportRecipientType.EMAIL,
+                    "recipient_config_json": {"target": "[email protected]"},
+                }
+            ],
+        }
+
+        exceptions: list[Exception] = []
+        command._populate_recipients(exceptions)
+
+        # Check that recipients were overridden
+        assert len(command._properties["recipients"]) == 1
+        assert command._properties["recipients"][0]["type"] == 
ReportRecipientType.EMAIL
+        assert (
+            
command._properties["recipients"][0]["recipient_config_json"]["target"]
+            == "[email protected]"
+        )
+        assert len(exceptions) == 0
+
+
+def test_populate_recipients_dashboard_creation_with_user_email() -> None:
+    """Test that dashboard creation uses current user's email."""
+    with patch("superset.commands.report.create.g") as mock_g:
+        # Setup user with email
+        mock_user = MagicMock()
+        mock_user.email = "[email protected]"
+        mock_g.user = mock_user
+
+        # Create command instance
+        command = CreateReportScheduleCommand({})
+        command._properties = {
+            "creation_method": ReportCreationMethod.DASHBOARDS,
+            # No recipients provided initially
+        }
+
+        exceptions: list[Exception] = []
+        command._populate_recipients(exceptions)
+
+        # Check that recipients were set
+        assert len(command._properties["recipients"]) == 1
+        assert command._properties["recipients"][0]["type"] == 
ReportRecipientType.EMAIL
+        assert (
+            
command._properties["recipients"][0]["recipient_config_json"]["target"]
+            == "[email protected]"
+        )
+        assert len(exceptions) == 0
+
+
+def test_populate_recipients_alerts_reports_keeps_original() -> None:
+    """Test that alerts_reports creation method preserves provided 
recipients."""
+    command = CreateReportScheduleCommand({})
+    original_recipients = [
+        {
+            "type": ReportRecipientType.EMAIL,
+            "recipient_config_json": {"target": "[email protected]"},
+        },
+        {
+            "type": ReportRecipientType.SLACK,
+            "recipient_config_json": {"target": "#alerts"},
+        },
+    ]
+    command._properties = {
+        "creation_method": ReportCreationMethod.ALERTS_REPORTS,
+        "recipients": original_recipients,
+    }
+
+    exceptions: list[Exception] = []
+    command._populate_recipients(exceptions)
+
+    # Check that recipients were NOT changed
+    assert command._properties["recipients"] == original_recipients
+    assert len(exceptions) == 0
+
+
+def test_populate_recipients_chart_creation_no_user_email() -> None:
+    """Test that chart creation fails when user has no email."""
+    with patch("superset.commands.report.create.g") as mock_g:
+        # Setup user without email
+        mock_user = MagicMock()
+        mock_user.email = None
+        mock_g.user = mock_user
+
+        command = CreateReportScheduleCommand({})
+        command._properties = {
+            "creation_method": ReportCreationMethod.CHARTS,
+        }
+
+        exceptions: list[Exception] = []
+        command._populate_recipients(exceptions)
+
+        # Check that validation error was added
+        assert len(exceptions) == 1
+        assert isinstance(exceptions[0], ReportScheduleUserEmailNotFoundError)
+        # Recipients should not be set
+        assert (
+            "recipients" not in command._properties
+            or command._properties["recipients"] == []
+        )
+
+
+def test_populate_recipients_dashboard_creation_no_user() -> None:
+    """Test that dashboard creation fails when there's no user."""
+    with patch("superset.commands.report.create.g") as mock_g:
+        # No user in context
+        mock_g.user = None
+
+        command = CreateReportScheduleCommand({})
+        command._properties = {
+            "creation_method": ReportCreationMethod.DASHBOARDS,
+        }
+
+        exceptions: list[Exception] = []
+        command._populate_recipients(exceptions)
+
+        # Check that validation error was added
+        assert len(exceptions) == 1
+        assert isinstance(exceptions[0], ReportScheduleUserEmailNotFoundError)
+
+
+def test_populate_recipients_no_creation_method() -> None:
+    """Test that recipients are unchanged when no creation_method is 
specified."""
+    command = CreateReportScheduleCommand({})
+    original_recipients = [
+        {
+            "type": ReportRecipientType.EMAIL,
+            "recipient_config_json": {"target": "[email protected]"},
+        }
+    ]
+    command._properties = {
+        # No creation_method specified
+        "recipients": original_recipients,
+    }
+
+    exceptions: list[Exception] = []
+    command._populate_recipients(exceptions)
+
+    # Check that recipients were NOT changed
+    assert command._properties["recipients"] == original_recipients
+    assert len(exceptions) == 0

Reply via email to