chore: Perform feature/config condition checks at request time (#14684)

* chore: conditional Home link rendering

* chore: conditional RowLevelSecurity rendering

* chore: Conditional KV rendering

* chore: Conditional TagView rendering

* chore: Conditional import dashboards link

* chore: Conditional upload csv/excel links

* chore: Conditional log api and view rendering

* chore: Conditionally render email schedules

* chore: Conditionally render alert views

* chore: Conditionally render alerts/reports

* chore: Conditionally render access requests

* chore: Conditionally render druid views

* Remove unnecessary folder

* Consistent naming

* Cleanup

* Remove object from class

* Clean up test file

* Clean up test file

* Fix lint error

* Better naming and follow conventions

* Use assertLess over assertNotEqual

* Assert less than 400

* Fix failing test
This commit is contained in:
Ben Reinhart 2021-05-21 14:29:52 -07:00 committed by GitHub
parent 1e8b6eb848
commit d5c008dd99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 593 additions and 196 deletions

View File

@ -226,10 +226,12 @@ class SupersetAppInitializer:
#
# Setup regular views
#
if appbuilder.app.config["LOGO_TARGET_PATH"]:
appbuilder.add_link(
"Home", label=__("Home"), href="/superset/welcome/",
)
appbuilder.add_link(
"Home",
label=__("Home"),
href="/superset/welcome/",
cond=lambda: bool(appbuilder.app.config["LOGO_TARGET_PATH"]),
)
appbuilder.add_view(
AnnotationLayerModelView,
"Annotation Layers",
@ -294,15 +296,17 @@ class SupersetAppInitializer:
category_label=__("Manage"),
category_icon="",
)
if feature_flag_manager.is_feature_enabled("ROW_LEVEL_SECURITY"):
appbuilder.add_view(
RowLevelSecurityFiltersModelView,
"Row Level Security",
label=__("Row level security"),
category="Security",
category_label=__("Security"),
icon="fa-lock",
)
appbuilder.add_view(
RowLevelSecurityFiltersModelView,
"Row Level Security",
label=__("Row level security"),
category="Security",
category_label=__("Security"),
icon="fa-lock",
menu_cond=lambda: feature_flag_manager.is_feature_enabled(
"ROW_LEVEL_SECURITY"
),
)
#
# Setup views with no menu
@ -314,10 +318,7 @@ class SupersetAppInitializer:
appbuilder.add_view_no_menu(Dashboard)
appbuilder.add_view_no_menu(DashboardModelViewAsync)
appbuilder.add_view_no_menu(Datasource)
if feature_flag_manager.is_feature_enabled("KV_STORE"):
appbuilder.add_view_no_menu(KV)
appbuilder.add_view_no_menu(KV)
appbuilder.add_view_no_menu(R)
appbuilder.add_view_no_menu(SavedQueryView)
appbuilder.add_view_no_menu(SavedQueryViewApi)
@ -330,23 +331,23 @@ class SupersetAppInitializer:
appbuilder.add_view_no_menu(TableModelView)
appbuilder.add_view_no_menu(TableSchemaView)
appbuilder.add_view_no_menu(TabStateView)
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
appbuilder.add_view_no_menu(TagView)
appbuilder.add_view_no_menu(TagView)
#
# Add links
#
if not feature_flag_manager.is_feature_enabled("VERSIONED_EXPORT"):
appbuilder.add_link(
"Import Dashboards",
label=__("Import Dashboards"),
href="/superset/import_dashboards/",
icon="fa-cloud-upload",
category="Manage",
category_label=__("Manage"),
category_icon="fa-wrench",
)
appbuilder.add_link(
"Import Dashboards",
label=__("Import Dashboards"),
href="/superset/import_dashboards/",
icon="fa-cloud-upload",
category="Manage",
category_label=__("Manage"),
category_icon="fa-wrench",
cond=lambda: not feature_flag_manager.is_feature_enabled(
"VERSIONED_EXPORT"
),
)
appbuilder.add_link(
"SQL Editor",
label=_("SQL Editor"),
@ -371,49 +372,54 @@ class SupersetAppInitializer:
category="SQL Lab",
category_label=__("SQL Lab"),
)
if self.config["CSV_EXTENSIONS"].intersection(
self.config["ALLOWED_EXTENSIONS"]
):
appbuilder.add_link(
"Upload a CSV",
label=__("Upload a CSV"),
href="/csvtodatabaseview/form",
icon="fa-upload",
category="Data",
category_label=__("Data"),
category_icon="fa-wrench",
cond=lambda: bool(
self.config["CSV_EXTENSIONS"].intersection(
self.config["ALLOWED_EXTENSIONS"]
)
),
)
try:
import xlrd # pylint: disable=unused-import
appbuilder.add_link(
"Upload a CSV",
label=__("Upload a CSV"),
href="/csvtodatabaseview/form",
"Upload Excel",
label=__("Upload Excel"),
href="/exceltodatabaseview/form",
icon="fa-upload",
category="Data",
category_label=__("Data"),
category_icon="fa-wrench",
cond=lambda: bool(
self.config["EXCEL_EXTENSIONS"].intersection(
self.config["ALLOWED_EXTENSIONS"]
)
),
)
try:
import xlrd # pylint: disable=unused-import
if self.config["EXCEL_EXTENSIONS"].intersection(
self.config["ALLOWED_EXTENSIONS"]
):
appbuilder.add_link(
"Upload Excel",
label=__("Upload Excel"),
href="/exceltodatabaseview/form",
icon="fa-upload",
category="Data",
category_label=__("Data"),
category_icon="fa-wrench",
)
except ImportError:
pass
#
# Conditionally setup log views
#
if self.config["FAB_ADD_SECURITY_VIEWS"] and self.config["SUPERSET_LOG_VIEW"]:
appbuilder.add_api(LogRestApi)
appbuilder.add_view(
LogModelView,
"Action Log",
label=__("Action Log"),
category="Security",
category_label=__("Security"),
icon="fa-list-ol",
)
appbuilder.add_api(LogRestApi)
appbuilder.add_view(
LogModelView,
"Action Log",
label=__("Action Log"),
category="Security",
category_label=__("Security"),
icon="fa-list-ol",
menu_cond=lambda: (
self.config["FAB_ADD_SECURITY_VIEWS"]
and self.config["SUPERSET_LOG_VIEW"]
),
)
appbuilder.add_api(SecurityRestApi)
#
# Conditionally setup email views
@ -423,109 +429,125 @@ class SupersetAppInitializer:
"ENABLE_SCHEDULED_EMAIL_REPORTS "
"is deprecated and will be removed in version 2.0.0"
)
appbuilder.add_separator("Manage")
appbuilder.add_view(
DashboardEmailScheduleView,
"Dashboard Email Schedules",
label=__("Dashboard Emails"),
category="Manage",
category_label=__("Manage"),
icon="fa-search",
)
appbuilder.add_view(
SliceEmailScheduleView,
"Chart Emails",
label=__("Chart Email Schedules"),
category="Manage",
category_label=__("Manage"),
icon="fa-search",
)
appbuilder.add_separator(
"Manage", cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"]
)
appbuilder.add_view(
DashboardEmailScheduleView,
"Dashboard Email Schedules",
label=__("Dashboard Emails"),
category="Manage",
category_label=__("Manage"),
icon="fa-search",
menu_cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"],
)
appbuilder.add_view(
SliceEmailScheduleView,
"Chart Emails",
label=__("Chart Email Schedules"),
category="Manage",
category_label=__("Manage"),
icon="fa-search",
menu_cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"],
)
if self.config["ENABLE_ALERTS"]:
logging.warning(
"ENABLE_ALERTS is deprecated and will be removed in version 2.0.0"
)
appbuilder.add_view(
AlertModelView,
"Alerts",
label=__("Alerts"),
category="Manage",
category_label=__("Manage"),
icon="fa-exclamation-triangle",
)
appbuilder.add_view_no_menu(AlertLogModelView)
appbuilder.add_view_no_menu(AlertObservationModelView)
if feature_flag_manager.is_feature_enabled("ALERT_REPORTS"):
appbuilder.add_view(
AlertView,
"Alerts & Report",
label=__("Alerts & Reports"),
category="Manage",
category_label=__("Manage"),
icon="fa-exclamation-triangle",
)
appbuilder.add_view_no_menu(ReportView)
appbuilder.add_view(
AlertModelView,
"Alerts",
label=__("Alerts"),
category="Manage",
category_label=__("Manage"),
icon="fa-exclamation-triangle",
menu_cond=lambda: bool(self.config["ENABLE_ALERTS"]),
)
appbuilder.add_view_no_menu(AlertLogModelView)
appbuilder.add_view_no_menu(AlertObservationModelView)
appbuilder.add_view(
AlertView,
"Alerts & Report",
label=__("Alerts & Reports"),
category="Manage",
category_label=__("Manage"),
icon="fa-exclamation-triangle",
menu_cond=lambda: feature_flag_manager.is_feature_enabled("ALERT_REPORTS"),
)
appbuilder.add_view_no_menu(ReportView)
appbuilder.add_view(
AccessRequestsModelView,
"Access requests",
label=__("Access requests"),
category="Security",
category_label=__("Security"),
icon="fa-table",
menu_cond=lambda: bool(self.config["ENABLE_ACCESS_REQUEST"]),
)
#
# Conditionally add Access Request Model View
# Druid Views
#
if self.config["ENABLE_ACCESS_REQUEST"]:
appbuilder.add_view(
AccessRequestsModelView,
"Access requests",
label=__("Access requests"),
category="Security",
category_label=__("Security"),
icon="fa-table",
)
appbuilder.add_separator(
"Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"])
)
appbuilder.add_view(
DruidDatasourceModelView,
"Druid Datasources",
label=__("Druid Datasources"),
category="Data",
category_label=__("Data"),
icon="fa-cube",
menu_cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]),
)
appbuilder.add_view(
DruidClusterModelView,
name="Druid Clusters",
label=__("Druid Clusters"),
icon="fa-cubes",
category="Data",
category_label=__("Data"),
category_icon="fa-database",
menu_cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]),
)
appbuilder.add_view_no_menu(DruidMetricInlineView)
appbuilder.add_view_no_menu(DruidColumnInlineView)
appbuilder.add_view_no_menu(Druid)
#
# Conditionally setup Druid Views
#
if self.config["DRUID_IS_ACTIVE"]:
appbuilder.add_separator("Data")
appbuilder.add_view(
DruidDatasourceModelView,
"Druid Datasources",
label=__("Druid Datasources"),
category="Data",
category_label=__("Data"),
icon="fa-cube",
)
appbuilder.add_view(
DruidClusterModelView,
name="Druid Clusters",
label=__("Druid Clusters"),
icon="fa-cubes",
category="Data",
category_label=__("Data"),
category_icon="fa-database",
)
appbuilder.add_view_no_menu(DruidMetricInlineView)
appbuilder.add_view_no_menu(DruidColumnInlineView)
appbuilder.add_view_no_menu(Druid)
if self.config["DRUID_METADATA_LINKS_ENABLED"]:
appbuilder.add_link(
"Scan New Datasources",
label=__("Scan New Datasources"),
href="/druid/scan_new_datasources/",
category="Data",
category_label=__("Data"),
category_icon="fa-database",
icon="fa-refresh",
)
appbuilder.add_link(
"Refresh Druid Metadata",
label=__("Refresh Druid Metadata"),
href="/druid/refresh_datasources/",
category="Data",
category_label=__("Data"),
category_icon="fa-database",
icon="fa-cog",
)
appbuilder.add_separator("Data")
appbuilder.add_link(
"Scan New Datasources",
label=__("Scan New Datasources"),
href="/druid/scan_new_datasources/",
category="Data",
category_label=__("Data"),
category_icon="fa-database",
icon="fa-refresh",
cond=lambda: bool(
self.config["DRUID_IS_ACTIVE"]
and self.config["DRUID_METADATA_LINKS_ENABLED"]
),
)
appbuilder.add_link(
"Refresh Druid Metadata",
label=__("Refresh Druid Metadata"),
href="/druid/refresh_datasources/",
category="Data",
category_label=__("Data"),
category_icon="fa-database",
icon="fa-cog",
cond=lambda: bool(
self.config["DRUID_IS_ACTIVE"]
and self.config["DRUID_METADATA_LINKS_ENABLED"]
),
)
appbuilder.add_separator(
"Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"])
)
def init_app_in_ctx(self) -> None:
"""

View File

@ -19,12 +19,14 @@ import json
import logging
from datetime import datetime
from flask import flash, Markup, redirect
from flask import current_app as app, flash, Markup, redirect
from flask_appbuilder import CompactCRUDMixin, expose
from flask_appbuilder.fieldwidgets import Select2Widget
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access
from flask_babel import lazy_gettext as _
from werkzeug.exceptions import NotFound
from wtforms import StringField
from wtforms.ext.sqlalchemy.fields import QuerySelectField
@ -49,7 +51,18 @@ from superset.views.base import (
logger = logging.getLogger(__name__)
class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView):
class EnsureEnabledMixin:
@staticmethod
def is_enabled() -> bool:
return bool(app.config["DRUID_IS_ACTIVE"])
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
class DruidColumnInlineView(CompactCRUDMixin, EnsureEnabledMixin, SupersetModelView):
datamodel = SQLAInterface(models.DruidColumn)
include_route_methods = RouteMethod.RELATED_VIEW_SET
@ -136,7 +149,7 @@ class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView):
self.post_update(item)
class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView):
class DruidMetricInlineView(CompactCRUDMixin, EnsureEnabledMixin, SupersetModelView):
datamodel = SQLAInterface(models.DruidMetric)
include_route_methods = RouteMethod.RELATED_VIEW_SET
@ -189,7 +202,9 @@ class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView):
edit_form_extra_fields = add_form_extra_fields
class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin):
class DruidClusterModelView(
EnsureEnabledMixin, SupersetModelView, DeleteMixin, YamlExportMixin,
):
datamodel = SQLAInterface(models.DruidCluster)
include_route_methods = RouteMethod.CRUD_SET
list_title = _("Druid Clusters")
@ -251,7 +266,9 @@ class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin):
DeleteMixin._delete(self, pk)
class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin):
class DruidDatasourceModelView(
EnsureEnabledMixin, DatasourceModelView, DeleteMixin, YamlExportMixin,
):
datamodel = SQLAInterface(models.DruidDatasource)
include_route_methods = RouteMethod.CRUD_SET
list_title = _("Druid Datasources")
@ -367,7 +384,7 @@ class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin
DeleteMixin._delete(self, pk)
class Druid(BaseSupersetView):
class Druid(EnsureEnabledMixin, BaseSupersetView):
"""The base views for Superset!"""
@has_access

View File

@ -24,9 +24,11 @@ from flask import current_app, flash, Markup, redirect
from flask_appbuilder import CompactCRUDMixin, expose
from flask_appbuilder.actions import action
from flask_appbuilder.fieldwidgets import Select2Widget
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access
from flask_babel import gettext as __, lazy_gettext as _
from werkzeug.exceptions import NotFound
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from wtforms.validators import Regexp
@ -369,6 +371,15 @@ class RowLevelSecurityFiltersModelView( # pylint: disable=too-many-ancestors
add_form_query_rel_fields = app.config["RLS_FORM_QUERY_REL_FIELDS"]
edit_form_query_rel_fields = add_form_query_rel_fields
@staticmethod
def is_enabled() -> bool:
return is_feature_enabled("ROW_LEVEL_SECURITY")
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
class TableModelView( # pylint: disable=too-many-ancestors
DatasourceModelView, DeleteMixin, YamlExportMixin

View File

@ -14,8 +14,11 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from flask import current_app as app
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import lazy_gettext as _
from werkzeug.exceptions import NotFound
from superset.constants import RouteMethod
from superset.views.base import DeleteMixin, SupersetModelView
@ -44,3 +47,12 @@ class AccessRequestsModelView( # pylint: disable=too-many-ancestors
"roles_with_datasource": _("Roles to grant"),
"created_on": _("Created On"),
}
@staticmethod
def is_enabled() -> bool:
return bool(app.config["ENABLE_ACCESS_REQUEST"])
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()

View File

@ -17,14 +17,15 @@
"""
DEPRECATION NOTICE: this module is deprecated and will be removed on 2.0.
"""
from croniter import croniter
from flask import abort, flash, Markup
from flask import abort, current_app as app, flash, Markup
from flask_appbuilder import CompactCRUDMixin, permission_name
from flask_appbuilder.api import expose
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access
from flask_babel import lazy_gettext as _
from werkzeug.exceptions import NotFound
from superset import is_feature_enabled
from superset.constants import RouteMethod
@ -40,8 +41,19 @@ from .base import BaseSupersetView, SupersetModelView
# TODO: access control rules for this module
class EnsureEnabledMixin:
@staticmethod
def is_enabled() -> bool:
return bool(app.config["ENABLE_ALERTS"])
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
class AlertLogModelView(
CompactCRUDMixin, SupersetModelView
CompactCRUDMixin, EnsureEnabledMixin, SupersetModelView
): # pylint: disable=too-many-ancestors
datamodel = SQLAInterface(AlertLog)
include_route_methods = {RouteMethod.LIST} | {"show"}
@ -55,7 +67,7 @@ class AlertLogModelView(
class AlertObservationModelView(
CompactCRUDMixin, SupersetModelView
CompactCRUDMixin, EnsureEnabledMixin, SupersetModelView
): # pylint: disable=too-many-ancestors
datamodel = SQLAInterface(SQLObservation)
include_route_methods = {RouteMethod.LIST} | {"show"}
@ -110,7 +122,9 @@ class ReportView(BaseAlertReportView):
class_permission_name = "ReportSchedule"
class AlertModelView(SupersetModelView): # pylint: disable=too-many-ancestors
class AlertModelView(
EnsureEnabledMixin, SupersetModelView
): # pylint: disable=too-many-ancestors
datamodel = SQLAInterface(Alert)
route_base = "/alerts"
include_route_methods = RouteMethod.CRUD_SET | {"log"}

View File

@ -17,9 +17,11 @@
import simplejson as json
from flask import request, Response
from flask_appbuilder import expose
from flask_appbuilder.hooks import before_request
from flask_appbuilder.security.decorators import has_access_api
from werkzeug.exceptions import NotFound
from superset import db, event_logger
from superset import db, event_logger, is_feature_enabled
from superset.models import core as models
from superset.typing import FlaskResponse
from superset.utils import core as utils
@ -30,6 +32,15 @@ class KV(BaseSupersetView):
"""Used for storing and retrieving key value pairs"""
@staticmethod
def is_enabled() -> bool:
return is_feature_enabled("KV_STORE")
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
@event_logger.log_this
@has_access_api
@expose("/store/", methods=["POST"])

View File

@ -14,6 +14,8 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from flask import current_app as app
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
import superset.models.core as models
@ -42,3 +44,13 @@ class LogRestApi(LogMixin, BaseSupersetModelRestApi):
"referrer",
]
show_columns = list_columns
@staticmethod
def is_enabled() -> bool:
return app.config["FAB_ADD_SECURITY_VIEWS"] and app.config["SUPERSET_LOG_VIEW"]
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
return self.response_404()
return None

View File

@ -14,7 +14,10 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from flask import current_app as app
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from werkzeug.exceptions import NotFound
import superset.models.core as models
from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod
@ -28,3 +31,12 @@ class LogModelView(LogMixin, SupersetModelView): # pylint: disable=too-many-anc
include_route_methods = {RouteMethod.LIST, RouteMethod.SHOW}
class_permission_name = "Log"
method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
@staticmethod
def is_enabled() -> bool:
return app.config["FAB_ADD_SECURITY_VIEWS"] and app.config["SUPERSET_LOG_VIEW"]
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()

View File

@ -23,11 +23,13 @@ from typing import Type, Union
import simplejson as json
from croniter import croniter
from flask import flash, g, Markup
from flask import current_app as app, flash, g, Markup
from flask_appbuilder import expose
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access
from flask_babel import lazy_gettext as _
from werkzeug.exceptions import NotFound
from wtforms import BooleanField, Form, StringField
from superset import db, security_manager
@ -54,6 +56,15 @@ class EmailScheduleView(
include_route_methods = RouteMethod.CRUD_SET
_extra_data = {"test_email": False, "test_email_recipients": None}
@staticmethod
def is_enabled() -> bool:
return app.config["ENABLE_SCHEDULED_EMAIL_REPORTS"]
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
@property
def schedule_type(self) -> str:
raise NotImplementedError()

View File

@ -21,11 +21,13 @@ from typing import Any, Dict, List
import simplejson as json
from flask import request, Response
from flask_appbuilder import expose
from flask_appbuilder.hooks import before_request
from flask_appbuilder.security.decorators import has_access_api
from jinja2.sandbox import SandboxedEnvironment
from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
from superset import db, utils
from superset import db, is_feature_enabled, utils
from superset.jinja_context import ExtraCache
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
@ -47,6 +49,15 @@ def process_template(content: str) -> str:
class TagView(BaseSupersetView):
@staticmethod
def is_enabled() -> bool:
return is_feature_enabled("TAGGING_SYSTEM")
@before_request
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
@has_access_api
@expose("/tags/suggestions/", methods=["GET"])
def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use

View File

@ -17,7 +17,6 @@
"""Unit tests for alerting in Superset"""
import json
import logging
from typing import Optional
from unittest.mock import patch
import pytest
@ -41,6 +40,12 @@ from superset.tasks.schedules import (
validate_observations,
)
from superset.utils import core as utils
from superset.views.alerts import (
AlertLogModelView,
AlertModelView,
AlertObservationModelView,
)
from tests.base_tests import SupersetTestCase
from tests.test_app import app
from tests.utils import read_fixture
@ -359,3 +364,47 @@ def test_deliver_alert_screenshot(
"|*Explore in Superset*>",
"title": f"[Alert] {alert.label}",
}
class TestAlertsEndpoints(SupersetTestCase):
def test_log_model_view_disabled(self):
with patch.object(AlertLogModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/alertlogmodelview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_log_model_view_enabled(self):
with patch.object(AlertLogModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/alertlogmodelview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_model_view_disabled(self):
with patch.object(AlertModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/alerts/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_model_view_enabled(self):
with patch.object(AlertModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/alerts/list/"
rv = self.client.get(uri)
self.assertNotEqual(rv.status_code, 404)
def test_observation_view_disabled(self):
with patch.object(AlertObservationModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/alertobservationmodelview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_observation_view_enabled(self):
with patch.object(AlertObservationModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/alertobservationmodelview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)

View File

@ -32,23 +32,22 @@ import pytz
import random
import re
import unittest
from unittest import mock, skipUnless
from unittest import mock
import pandas as pd
import sqlalchemy as sqla
from sqlalchemy.exc import SQLAlchemyError
from superset.models.cache import CacheKey
from superset.utils.core import get_example_database
from tests.conftest import with_feature_flags
from tests.fixtures.energy_dashboard import load_energy_table_with_slice
from tests.test_app import app
import superset.views.utils
from superset import (
dataframe,
db,
jinja_context,
security_manager,
sql_lab,
is_feature_enabled,
)
from superset.connectors.sqla.models import SqlaTable
from superset.db_engine_specs.base import BaseEngineSpec
@ -657,10 +656,19 @@ class TestCore(SupersetTestCase):
db.session.delete(model_url)
db.session.commit()
@skipUnless(
(is_feature_enabled("KV_STORE")), "skipping as /kv/ endpoints are not enabled"
)
def test_kv(self):
@with_feature_flags(KV_STORE=False)
def test_kv_disabled(self):
self.login(username="admin")
resp = self.client.get("/kv/10001/")
self.assertEqual(404, resp.status_code)
value = json.dumps({"data": "this is a test"})
resp = self.client.post("/kv/store/", data=dict(data=value))
self.assertEqual(resp.status_code, 404)
@with_feature_flags(KV_STORE=True)
def test_kv_enabled(self):
self.login(username="admin")
resp = self.client.get("/kv/10001/")

View File

@ -24,6 +24,13 @@ from unittest.mock import Mock, patch
from tests.test_app import app
from superset import db, security_manager
from superset.connectors.druid.views import (
Druid,
DruidClusterModelView,
DruidColumnInlineView,
DruidDatasourceModelView,
DruidMetricInlineView,
)
from .base_tests import SupersetTestCase
@ -585,5 +592,77 @@ class TestDruid(SupersetTestCase):
self.assertEqual(col_names, {"__time", "dim1", "dim2", "metric1"})
class TestDruidViewEnabling(SupersetTestCase):
def test_druid_disabled(self):
with patch.object(Druid, "is_enabled", return_value=False):
self.login("admin")
uri = "/druid/refresh_datasources/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_druid_enabled(self):
with patch.object(Druid, "is_enabled", return_value=True):
self.login("admin")
uri = "/druid/refresh_datasources/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_druid_cluster_disabled(self):
with patch.object(DruidClusterModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/druidclustermodelview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_druid_cluster_enabled(self):
with patch.object(DruidClusterModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/druidclustermodelview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_druid_column_disabled(self):
with patch.object(DruidColumnInlineView, "is_enabled", return_value=False):
self.login("admin")
uri = "/druidcolumninlineview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_druid_column_enabled(self):
with patch.object(DruidColumnInlineView, "is_enabled", return_value=True):
self.login("admin")
uri = "/druidcolumninlineview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_druid_datasource_disabled(self):
with patch.object(DruidDatasourceModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/druiddatasourcemodelview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_druid_datasource_enabled(self):
with patch.object(DruidDatasourceModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/druiddatasourcemodelview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_druid_metric_disabled(self):
with patch.object(DruidMetricInlineView, "is_enabled", return_value=False):
self.login("admin")
uri = "/druidmetricinlineview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_druid_metric_enabled(self):
with patch.object(DruidMetricInlineView, "is_enabled", return_value=True):
self.login("admin")
uri = "/druidmetricinlineview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
if __name__ == "__main__":
unittest.main()

View File

@ -21,10 +21,11 @@ from typing import Optional
import prison
from flask_appbuilder.security.sqla.models import User
from unittest.mock import patch
import tests.test_app
from superset import db
from superset.models.core import Log
from superset.views.log.api import LogRestApi
from .base_tests import SupersetTestCase
@ -64,6 +65,16 @@ class TestLogApi(SupersetTestCase):
db.session.commit()
return log
def test_not_enabled(self):
with patch.object(LogRestApi, "is_enabled", return_value=False):
admin_user = self.get_user("admin")
self.insert_log("some_action", admin_user)
self.login(username="admin")
arguments = {"filters": [{"col": "action", "opr": "sw", "value": "some_"}]}
uri = f"api/v1/log/?q={prison.dumps(arguments)}"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_get_list(self):
"""
Log API: Test get list

View File

@ -0,0 +1,37 @@
# 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 patch
from superset.views.log.views import LogModelView
from .base_tests import SupersetTestCase
class TestLogModelView(SupersetTestCase):
def test_disabled(self):
with patch.object(LogModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/logmodelview/list/"
rv = self.client.get(uri)
self.assert404(rv)
def test_enabled(self):
with patch.object(LogModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/logmodelview/list/"
rv = self.client.get(uri)
self.assert200(rv)

View File

@ -16,6 +16,7 @@
# under the License.
# isort:skip_file
from datetime import datetime, timedelta
from superset.views.schedules import DashboardEmailScheduleView, SliceEmailScheduleView
from unittest.mock import Mock, patch, PropertyMock
from flask_babel import gettext as __
@ -555,6 +556,34 @@ class TestSchedules(SupersetTestCase):
},
)
def test_dashboard_disabled(self):
with patch.object(DashboardEmailScheduleView, "is_enabled", return_value=False):
self.login("admin")
uri = "/dashboardemailscheduleview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_dashboard_enabled(self):
with patch.object(DashboardEmailScheduleView, "is_enabled", return_value=True):
self.login("admin")
uri = "/dashboardemailscheduleview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_slice_disabled(self):
with patch.object(SliceEmailScheduleView, "is_enabled", return_value=False):
self.login("admin")
uri = "/sliceemailscheduleview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_slice_enabled(self):
with patch.object(SliceEmailScheduleView, "is_enabled", return_value=True):
self.login("admin")
uri = "/sliceemailscheduleview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)
def test_slack_client_compatibility():
c2 = WebClient()

View File

@ -38,6 +38,7 @@ from superset.models.core import Database
from superset.models.slice import Slice
from superset.sql_parse import Table
from superset.utils.core import get_example_database
from superset.views.access_requests import AccessRequestsModelView
from .base_tests import SupersetTestCase
from tests.fixtures.birth_names_dashboard import load_birth_names_dashboard_with_slices
@ -1203,3 +1204,19 @@ class TestRowLevelSecurity(SupersetTestCase):
assert not self.NAMES_B_REGEX.search(sql)
assert not self.NAMES_Q_REGEX.search(sql)
assert not self.BASE_FILTER_REGEX.search(sql)
class TestAccessRequestEndpoints(SupersetTestCase):
def test_access_request_disabled(self):
with patch.object(AccessRequestsModelView, "is_enabled", return_value=False):
self.login("admin")
uri = "/accessrequestsmodelview/list/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
def test_access_request_enabled(self):
with patch.object(AccessRequestsModelView, "is_enabled", return_value=True):
self.login("admin")
uri = "/accessrequestsmodelview/list/"
rv = self.client.get(uri)
self.assertLess(rv.status_code, 400)

40
tests/sqla_views_tests.py Normal file
View File

@ -0,0 +1,40 @@
# 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 tests.base_tests import SupersetTestCase
from tests.conftest import with_feature_flags
class TestRowLevelSecurityFiltersModelView(SupersetTestCase):
@with_feature_flags(ROW_LEVEL_SECURITY=False)
def test_rls_disabled(self):
"""
RLS Filters Model View: Responds not found when disabled
"""
self.login(username="admin")
uri = "/rowlevelsecurityfiltersmodelview/api"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)
@with_feature_flags(ROW_LEVEL_SECURITY=True)
def test_rls_enabled(self):
"""
RLS Filters Model View: Responds successfully when enabled
"""
self.login(username="admin")
uri = "/rowlevelsecurityfiltersmodelview/api"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200)

View File

@ -15,25 +15,19 @@
# specific language governing permissions and limitations
# under the License.
from unittest import skipUnless
from superset import is_feature_enabled
from tests.base_tests import SupersetTestCase
from tests.conftest import with_feature_flags
class TestTagging(SupersetTestCase):
@skipUnless(
(is_feature_enabled("TAGGING_SYSTEM") == False),
"skipping as tagging endpoints are not enabled",
)
def test_tag_view_attachment_disabled(self):
response = self.client.get("/tagview/tagged_objects/")
@with_feature_flags(TAGGING_SYSTEM=False)
def test_tag_view_disabled(self):
self.login("admin")
response = self.client.get("/tagview/tags/suggestions/")
self.assertEqual(404, response.status_code)
@skipUnless(
is_feature_enabled("TAGGING_SYSTEM"),
"skipping as tagging endpoints are enabled",
)
def test_tag_view_attachment_enabled(self):
response = self.client.get("/tagview/tagged_objects/")
self.assertEqual(200, response.status_code)
@with_feature_flags(TAGGING_SYSTEM=True)
def test_tag_view_enabled(self):
self.login("admin")
response = self.client.get("/tagview/tags/suggestions/")
self.assertNotEqual(404, response.status_code)