feat(reports): execute as other than selenium user (#21931)

Co-authored-by: Ville Brofeldt <ville.brofeldt@apple.com>
This commit is contained in:
Ville Brofeldt
2022-10-31 14:32:49 +02:00
committed by GitHub
parent c9470cac91
commit a02a778cc3
14 changed files with 517 additions and 87 deletions

View File

@@ -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
)

View File

@@ -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")

View File

@@ -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,