mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
feat(reports): execute as other than selenium user (#21931)
Co-authored-by: Ville Brofeldt <ville.brofeldt@apple.com>
This commit is contained in:
@@ -23,6 +23,7 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from flask import current_app
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
from flask_sqlalchemy import BaseQuery
|
||||
from freezegun import freeze_time
|
||||
from sqlalchemy.sql import func
|
||||
@@ -55,6 +56,7 @@ from superset.reports.models import (
|
||||
ReportScheduleValidatorType,
|
||||
ReportState,
|
||||
)
|
||||
from superset.reports.types import ReportScheduleExecutor
|
||||
from superset.utils.database import get_example_database
|
||||
from tests.integration_tests.fixtures.birth_names_dashboard import (
|
||||
load_birth_names_dashboard_with_slices,
|
||||
@@ -68,7 +70,7 @@ from tests.integration_tests.reports.utils import (
|
||||
cleanup_report_schedule,
|
||||
create_report_notification,
|
||||
CSV_FILE,
|
||||
OWNER_EMAIL,
|
||||
DEFAULT_OWNER_EMAIL,
|
||||
SCREENSHOT_FILE,
|
||||
TEST_ID,
|
||||
)
|
||||
@@ -152,6 +154,19 @@ def create_report_email_chart():
|
||||
cleanup_report_schedule(report_schedule)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def create_report_email_chart_alpha_owner(get_user):
|
||||
with app.app_context():
|
||||
owners = [get_user("alpha")]
|
||||
chart = db.session.query(Slice).first()
|
||||
report_schedule = create_report_notification(
|
||||
email_target="target@email.com", chart=chart, owners=owners
|
||||
)
|
||||
yield report_schedule
|
||||
|
||||
cleanup_report_schedule(report_schedule)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def create_report_email_chart_force_screenshot():
|
||||
with app.app_context():
|
||||
@@ -645,6 +660,65 @@ def test_email_chart_report_schedule(
|
||||
assert_log(ReportState.SUCCESS)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"load_birth_names_dashboard_with_slices", "create_report_email_chart_alpha_owner"
|
||||
)
|
||||
@patch("superset.reports.notifications.email.send_email_smtp")
|
||||
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
|
||||
def test_email_chart_report_schedule_alpha_owner(
|
||||
screenshot_mock,
|
||||
email_mock,
|
||||
create_report_email_chart_alpha_owner,
|
||||
):
|
||||
"""
|
||||
ExecuteReport Command: Test chart email report schedule with screenshot
|
||||
executed as the chart owner
|
||||
"""
|
||||
config_key = "ALERT_REPORTS_EXECUTE_AS"
|
||||
original_config_value = app.config[config_key]
|
||||
app.config[config_key] = [ReportScheduleExecutor.OWNER]
|
||||
|
||||
# setup screenshot mock
|
||||
username = ""
|
||||
|
||||
def _screenshot_side_effect(user: User) -> Optional[bytes]:
|
||||
nonlocal username
|
||||
username = user.username
|
||||
|
||||
return SCREENSHOT_FILE
|
||||
|
||||
screenshot_mock.side_effect = _screenshot_side_effect
|
||||
|
||||
with freeze_time("2020-01-01T00:00:00Z"):
|
||||
AsyncExecuteReportScheduleCommand(
|
||||
TEST_ID, create_report_email_chart_alpha_owner.id, datetime.utcnow()
|
||||
).run()
|
||||
|
||||
notification_targets = get_target_from_report_schedule(
|
||||
create_report_email_chart_alpha_owner
|
||||
)
|
||||
# assert that the screenshot is executed as the chart owner
|
||||
assert username == "alpha"
|
||||
|
||||
# assert that the link sent is correct
|
||||
assert (
|
||||
'<a href="http://0.0.0.0:8080/explore/?'
|
||||
"form_data=%7B%22slice_id%22%3A%20"
|
||||
f"{create_report_email_chart_alpha_owner.chart.id}%7D&"
|
||||
'standalone=0&force=false">Explore in Superset</a>'
|
||||
in email_mock.call_args[0][2]
|
||||
)
|
||||
# Assert the email smtp address
|
||||
assert email_mock.call_args[0][0] == notification_targets[0]
|
||||
# Assert the email inline screenshot
|
||||
smtp_images = email_mock.call_args[1]["images"]
|
||||
assert smtp_images[list(smtp_images.keys())[0]] == SCREENSHOT_FILE
|
||||
# Assert logs are correct
|
||||
assert_log(ReportState.SUCCESS)
|
||||
|
||||
app.config[config_key] = original_config_value
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"load_birth_names_dashboard_with_slices",
|
||||
"create_report_email_chart_force_screenshot",
|
||||
@@ -1465,7 +1539,7 @@ def test_soft_timeout_alert(email_mock, create_alert_email_chart):
|
||||
|
||||
notification_targets = get_target_from_report_schedule(create_alert_email_chart)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
assert_log(
|
||||
ReportState.ERROR, error_message="A timeout occurred while executing the query."
|
||||
@@ -1494,7 +1568,7 @@ def test_soft_timeout_screenshot(screenshot_mock, email_mock, create_alert_email
|
||||
).run()
|
||||
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
assert_log(
|
||||
ReportState.ERROR, error_message="A timeout occurred while taking a screenshot."
|
||||
@@ -1534,7 +1608,7 @@ def test_soft_timeout_csv(
|
||||
create_report_email_chart_with_csv
|
||||
)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
assert_log(
|
||||
ReportState.ERROR,
|
||||
@@ -1574,7 +1648,7 @@ def test_generate_no_csv(
|
||||
create_report_email_chart_with_csv
|
||||
)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
assert_log(
|
||||
ReportState.ERROR,
|
||||
@@ -1603,7 +1677,7 @@ def test_fail_screenshot(screenshot_mock, email_mock, create_report_email_chart)
|
||||
|
||||
notification_targets = get_target_from_report_schedule(create_report_email_chart)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
assert_log(
|
||||
ReportState.ERROR, error_message="Failed taking a screenshot Unexpected error"
|
||||
@@ -1636,7 +1710,7 @@ def test_fail_csv(
|
||||
|
||||
get_target_from_report_schedule(create_report_email_chart_with_csv)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
assert_log(
|
||||
ReportState.ERROR, error_message="Failed generating csv <urlopen error 500>"
|
||||
@@ -1685,7 +1759,7 @@ def test_invalid_sql_alert(email_mock, create_invalid_sql_alert_email_chart):
|
||||
create_invalid_sql_alert_email_chart
|
||||
)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("create_invalid_sql_alert_email_chart")
|
||||
@@ -1706,7 +1780,7 @@ def test_grace_period_error(email_mock, create_invalid_sql_alert_email_chart):
|
||||
create_invalid_sql_alert_email_chart
|
||||
)
|
||||
# Assert the email smtp address, asserts a notification was sent with the error
|
||||
assert email_mock.call_args[0][0] == OWNER_EMAIL
|
||||
assert email_mock.call_args[0][0] == DEFAULT_OWNER_EMAIL
|
||||
assert (
|
||||
get_notification_error_sent_count(create_invalid_sql_alert_email_chart) == 1
|
||||
)
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from random import randint
|
||||
from typing import List
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
from freezegun import freeze_time
|
||||
from freezegun.api import FakeDatetime # type: ignore
|
||||
|
||||
@@ -27,8 +31,14 @@ from tests.integration_tests.reports.utils import insert_report_schedule
|
||||
from tests.integration_tests.test_app import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def owners(get_user) -> List[User]:
|
||||
return [get_user("admin")]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owners")
|
||||
@patch("superset.tasks.scheduler.execute.apply_async")
|
||||
def test_scheduler_celery_timeout_ny(execute_mock):
|
||||
def test_scheduler_celery_timeout_ny(execute_mock, owners):
|
||||
"""
|
||||
Reports scheduler: Test scheduler setting celery soft and hard timeout
|
||||
"""
|
||||
@@ -39,6 +49,7 @@ def test_scheduler_celery_timeout_ny(execute_mock):
|
||||
name="report",
|
||||
crontab="0 4 * * *",
|
||||
timezone="America/New_York",
|
||||
owners=owners,
|
||||
)
|
||||
|
||||
with freeze_time("2020-01-01T09:00:00Z"):
|
||||
@@ -49,8 +60,9 @@ def test_scheduler_celery_timeout_ny(execute_mock):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owners")
|
||||
@patch("superset.tasks.scheduler.execute.apply_async")
|
||||
def test_scheduler_celery_no_timeout_ny(execute_mock):
|
||||
def test_scheduler_celery_no_timeout_ny(execute_mock, owners):
|
||||
"""
|
||||
Reports scheduler: Test scheduler setting celery soft and hard timeout
|
||||
"""
|
||||
@@ -61,6 +73,7 @@ def test_scheduler_celery_no_timeout_ny(execute_mock):
|
||||
name="report",
|
||||
crontab="0 4 * * *",
|
||||
timezone="America/New_York",
|
||||
owners=owners,
|
||||
)
|
||||
|
||||
with freeze_time("2020-01-01T09:00:00Z"):
|
||||
@@ -71,8 +84,9 @@ def test_scheduler_celery_no_timeout_ny(execute_mock):
|
||||
app.config["ALERT_REPORTS_WORKING_TIME_OUT_KILL"] = True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owners")
|
||||
@patch("superset.tasks.scheduler.execute.apply_async")
|
||||
def test_scheduler_celery_timeout_utc(execute_mock):
|
||||
def test_scheduler_celery_timeout_utc(execute_mock, owners):
|
||||
"""
|
||||
Reports scheduler: Test scheduler setting celery soft and hard timeout
|
||||
"""
|
||||
@@ -83,6 +97,7 @@ def test_scheduler_celery_timeout_utc(execute_mock):
|
||||
name="report",
|
||||
crontab="0 9 * * *",
|
||||
timezone="UTC",
|
||||
owners=owners,
|
||||
)
|
||||
|
||||
with freeze_time("2020-01-01T09:00:00Z"):
|
||||
@@ -93,8 +108,9 @@ def test_scheduler_celery_timeout_utc(execute_mock):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owners")
|
||||
@patch("superset.tasks.scheduler.execute.apply_async")
|
||||
def test_scheduler_celery_no_timeout_utc(execute_mock):
|
||||
def test_scheduler_celery_no_timeout_utc(execute_mock, owners):
|
||||
"""
|
||||
Reports scheduler: Test scheduler setting celery soft and hard timeout
|
||||
"""
|
||||
@@ -105,6 +121,7 @@ def test_scheduler_celery_no_timeout_utc(execute_mock):
|
||||
name="report",
|
||||
crontab="0 9 * * *",
|
||||
timezone="UTC",
|
||||
owners=owners,
|
||||
)
|
||||
|
||||
with freeze_time("2020-01-01T09:00:00Z"):
|
||||
@@ -115,9 +132,10 @@ def test_scheduler_celery_no_timeout_utc(execute_mock):
|
||||
app.config["ALERT_REPORTS_WORKING_TIME_OUT_KILL"] = True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owners")
|
||||
@patch("superset.tasks.scheduler.is_feature_enabled")
|
||||
@patch("superset.tasks.scheduler.execute.apply_async")
|
||||
def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled):
|
||||
def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled, owners):
|
||||
"""
|
||||
Reports scheduler: Test scheduler with feature flag off
|
||||
"""
|
||||
@@ -128,6 +146,7 @@ def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled):
|
||||
name="report",
|
||||
crontab="0 9 * * *",
|
||||
timezone="UTC",
|
||||
owners=owners,
|
||||
)
|
||||
|
||||
with freeze_time("2020-01-01T09:00:00Z"):
|
||||
@@ -137,10 +156,11 @@ def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owners")
|
||||
@patch("superset.reports.commands.execute.AsyncExecuteReportScheduleCommand.__init__")
|
||||
@patch("superset.reports.commands.execute.AsyncExecuteReportScheduleCommand.run")
|
||||
@patch("superset.tasks.scheduler.execute.update_state")
|
||||
def test_execute_task(update_state_mock, command_mock, init_mock):
|
||||
def test_execute_task(update_state_mock, command_mock, init_mock, owners):
|
||||
from superset.reports.commands.exceptions import ReportScheduleUnexpectedError
|
||||
|
||||
with app.app_context():
|
||||
@@ -149,6 +169,7 @@ def test_execute_task(update_state_mock, command_mock, init_mock):
|
||||
name=f"report-{randint(0,1000)}",
|
||||
crontab="0 4 * * *",
|
||||
timezone="America/New_York",
|
||||
owners=owners,
|
||||
)
|
||||
init_mock.return_value = None
|
||||
command_mock.side_effect = ReportScheduleUnexpectedError("Unexpected error")
|
||||
|
||||
@@ -35,26 +35,27 @@ from superset.reports.models import (
|
||||
ReportScheduleType,
|
||||
ReportState,
|
||||
)
|
||||
from superset.utils.core import override_user
|
||||
from tests.integration_tests.test_app import app
|
||||
from tests.integration_tests.utils import read_fixture
|
||||
|
||||
TEST_ID = str(uuid4())
|
||||
CSV_FILE = read_fixture("trends.csv")
|
||||
SCREENSHOT_FILE = read_fixture("sample.png")
|
||||
OWNER_EMAIL = "admin@fab.org"
|
||||
DEFAULT_OWNER_EMAIL = "admin@fab.org"
|
||||
|
||||
|
||||
def insert_report_schedule(
|
||||
type: str,
|
||||
name: str,
|
||||
crontab: str,
|
||||
owners: List[User],
|
||||
timezone: Optional[str] = None,
|
||||
sql: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
chart: Optional[Slice] = None,
|
||||
dashboard: Optional[Dashboard] = None,
|
||||
database: Optional[Database] = None,
|
||||
owners: Optional[List[User]] = None,
|
||||
validator_type: Optional[str] = None,
|
||||
validator_config_json: Optional[str] = None,
|
||||
log_retention: Optional[int] = None,
|
||||
@@ -70,28 +71,30 @@ def insert_report_schedule(
|
||||
recipients = recipients or []
|
||||
logs = logs or []
|
||||
last_state = last_state or ReportState.NOOP
|
||||
report_schedule = ReportSchedule(
|
||||
type=type,
|
||||
name=name,
|
||||
crontab=crontab,
|
||||
timezone=timezone,
|
||||
sql=sql,
|
||||
description=description,
|
||||
chart=chart,
|
||||
dashboard=dashboard,
|
||||
database=database,
|
||||
owners=owners,
|
||||
validator_type=validator_type,
|
||||
validator_config_json=validator_config_json,
|
||||
log_retention=log_retention,
|
||||
grace_period=grace_period,
|
||||
recipients=recipients,
|
||||
logs=logs,
|
||||
last_state=last_state,
|
||||
report_format=report_format,
|
||||
extra=extra,
|
||||
force_screenshot=force_screenshot,
|
||||
)
|
||||
|
||||
with override_user(owners[0]):
|
||||
report_schedule = ReportSchedule(
|
||||
type=type,
|
||||
name=name,
|
||||
crontab=crontab,
|
||||
timezone=timezone,
|
||||
sql=sql,
|
||||
description=description,
|
||||
chart=chart,
|
||||
dashboard=dashboard,
|
||||
database=database,
|
||||
owners=owners,
|
||||
validator_type=validator_type,
|
||||
validator_config_json=validator_config_json,
|
||||
log_retention=log_retention,
|
||||
grace_period=grace_period,
|
||||
recipients=recipients,
|
||||
logs=logs,
|
||||
last_state=last_state,
|
||||
report_format=report_format,
|
||||
extra=extra,
|
||||
force_screenshot=force_screenshot,
|
||||
)
|
||||
db.session.add(report_schedule)
|
||||
db.session.commit()
|
||||
return report_schedule
|
||||
@@ -112,12 +115,16 @@ def create_report_notification(
|
||||
name: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
force_screenshot: bool = False,
|
||||
owners: Optional[List[User]] = None,
|
||||
) -> ReportSchedule:
|
||||
owner = (
|
||||
db.session.query(security_manager.user_model)
|
||||
.filter_by(email=OWNER_EMAIL)
|
||||
.one_or_none()
|
||||
)
|
||||
if not owners:
|
||||
owners = [
|
||||
(
|
||||
db.session.query(security_manager.user_model)
|
||||
.filter_by(email=DEFAULT_OWNER_EMAIL)
|
||||
.one_or_none()
|
||||
)
|
||||
]
|
||||
|
||||
if slack_channel:
|
||||
recipient = ReportRecipients(
|
||||
@@ -147,7 +154,7 @@ def create_report_notification(
|
||||
dashboard=dashboard,
|
||||
database=database,
|
||||
recipients=[recipient],
|
||||
owners=[owner],
|
||||
owners=owners,
|
||||
validator_type=validator_type,
|
||||
validator_config_json=validator_config_json,
|
||||
grace_period=grace_period,
|
||||
|
||||
Reference in New Issue
Block a user