From 315a11dfe265c6a20ed014610eb7351966292c38 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Thu, 23 Jan 2020 11:25:15 -0500 Subject: [PATCH] fix: shut off unneeded endpoints (#8960) * fix: shut off all uneeded endpoints We recently added a new feature to FAB allowing to whitelist the needed endpoints in ModelView and ModelRestApi. First, we set our base wrapper class to an empty set, forcing each class inheriting from it to explicitely turn on the endpoints that Superset intends to use. Second, we go ModelView by ModelView to whitelist the actual endpoints used in the app. Notes: * as a result a large set of [unneeded] permissions should be cleaned up * outside of the "private" use of endpoints in the app, people that have been using endpoints in their environment for other purposes may experience loss of functionality * Tweaking * Reduce the amount of endpoints using white lists * Fix, included needed endpoints for dashboard and druid * Drying things up * fixes * limiting more endpoints * Read only on some FAB model views * fixing some tests * fixes * Fixing more tests * Addressing comments * Drying up route_methods * further drying Co-authored-by: Daniel Vaz Gaspar --- .gitignore | 1 + .rat-excludes | 2 + requirements.txt | 2 +- superset/app.py | 50 +++++++---------- .../src/dashboard/actions/sliceEntities.js | 2 +- superset/connectors/druid/views.py | 7 ++- superset/connectors/sqla/views.py | 5 ++ superset/constants.py | 40 ++++++++++++++ superset/security/manager.py | 10 ++++ superset/views/annotations.py | 3 ++ superset/views/base_api.py | 11 ++++ superset/views/chart/api.py | 9 ---- superset/views/chart/views.py | 53 +++++++++---------- superset/views/core.py | 17 ++---- superset/views/dashboard/api.py | 18 +++---- superset/views/dashboard/views.py | 25 ++++----- superset/views/database/api.py | 15 ++---- superset/views/database/views.py | 22 +------- superset/views/log/api.py | 12 +---- superset/views/log/views.py | 2 + superset/views/schedules.py | 2 + superset/views/sql_lab.py | 9 ++++ tests/core_tests.py | 2 +- tests/import_export_tests.py | 7 ++- tests/security_tests.py | 40 +++----------- 25 files changed, 176 insertions(+), 190 deletions(-) diff --git a/.gitignore b/.gitignore index b228b9840..5dc500e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ *.pyc *.sqllite *.swp +.bento* .cache-loader .coverage .DS_Store diff --git a/.rat-excludes b/.rat-excludes index 61d68b54d..79ed787d2 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -29,7 +29,9 @@ apache_superset.egg-info .*json .*csv # Generated doc files +env/* docs/_build/* +docs/_modules/* _build/* _static/* .buildinfo diff --git a/requirements.txt b/requirements.txt index a89d85904..fb8414671 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ croniter==0.3.31 cryptography==2.8 decorator==4.4.1 # via retry defusedxml==0.6.0 # via python3-openid -flask-appbuilder==2.2.1 +flask-appbuilder==2.2.2rc3 flask-babel==0.12.2 # via flask-appbuilder flask-caching==1.8.0 flask-compress==1.4.0 diff --git a/superset/app.py b/superset/app.py index 98381977a..4a904eea1 100644 --- a/superset/app.py +++ b/superset/app.py @@ -149,21 +149,15 @@ class SupersetAppInitializer: CssTemplateAsyncModelView, ) from superset.views.chart.api import ChartRestApi - from superset.views.chart.views import SliceModelView, SliceAsync, SliceAddView + from superset.views.chart.views import SliceModelView, SliceAsync from superset.views.dashboard.api import DashboardRestApi from superset.views.dashboard.views import ( DashboardModelView, Dashboard, - DashboardAddView, DashboardModelViewAsync, ) from superset.views.database.api import DatabaseRestApi - from superset.views.database.views import ( - DatabaseView, - DatabaseTablesAsync, - CsvToDatabaseView, - DatabaseAsync, - ) + from superset.views.database.views import DatabaseView, CsvToDatabaseView from superset.views.datasource import Datasource from superset.views.log.api import LogRestApi from superset.views.log.views import LogModelView @@ -218,6 +212,16 @@ class SupersetAppInitializer: category_label=__("Sources"), category_icon="fa-database", ) + appbuilder.add_link( + "Tables", + label=__("Tables"), + href="/tablemodelview/list/?_flt_1_is_sqllab_view=y", + icon="fa-table", + category="Sources", + category_label=__("Sources"), + category_icon="fa-table", + ) + appbuilder.add_separator("Sources") appbuilder.add_view( SliceModelView, "Charts", @@ -259,16 +263,12 @@ class SupersetAppInitializer: appbuilder.add_view_no_menu(CssTemplateAsyncModelView) appbuilder.add_view_no_menu(CsvToDatabaseView) appbuilder.add_view_no_menu(Dashboard) - appbuilder.add_view_no_menu(DashboardAddView) appbuilder.add_view_no_menu(DashboardModelViewAsync) - appbuilder.add_view_no_menu(DatabaseAsync) - appbuilder.add_view_no_menu(DatabaseTablesAsync) appbuilder.add_view_no_menu(Datasource) appbuilder.add_view_no_menu(KV) appbuilder.add_view_no_menu(R) appbuilder.add_view_no_menu(SavedQueryView) appbuilder.add_view_no_menu(SavedQueryViewApi) - appbuilder.add_view_no_menu(SliceAddView) appbuilder.add_view_no_menu(SliceAsync) appbuilder.add_view_no_menu(SqlLab) appbuilder.add_view_no_menu(SqlMetricInlineView) @@ -282,12 +282,6 @@ class SupersetAppInitializer: # # Add links # - appbuilder.add_link( - __("Saved Queries"), - href="/sqllab/my_queries/", - icon="fa-save", - category="SQL Lab", - ) appbuilder.add_link( "Import Dashboards", label=__("Import Dashboards"), @@ -306,6 +300,12 @@ class SupersetAppInitializer: category="SQL Lab", category_label=__("SQL Lab"), ) + appbuilder.add_link( + __("Saved Queries"), + href="/sqllab/my_queries/", + icon="fa-save", + category="SQL Lab", + ) appbuilder.add_link( "Query Search", label=_("Query Search"), @@ -324,23 +324,11 @@ class SupersetAppInitializer: category_label=__("Sources"), category_icon="fa-wrench", ) - appbuilder.add_link( - "Tables", - label=__("Tables"), - href="/tablemodelview/list/?_flt_1_is_sqllab_view=y", - icon="fa-table", - category="Sources", - category_label=__("Sources"), - category_icon="fa-table", - ) # # Conditionally setup log views # - if ( - not self.config["FAB_ADD_SECURITY_VIEWS"] is False - or self.config["SUPERSET_LOG_VIEW"] is False - ): + if self.config["FAB_ADD_SECURITY_VIEWS"] and self.config["SUPERSET_LOG_VIEW"]: appbuilder.add_api(LogRestApi) appbuilder.add_view( LogModelView, diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js index efee23eec..b0db62614 100644 --- a/superset/assets/src/dashboard/actions/sliceEntities.js +++ b/superset/assets/src/dashboard/actions/sliceEntities.js @@ -46,7 +46,7 @@ export function fetchAllSlices(userId) { dispatch(fetchAllSlicesStarted()); return SupersetClient.get({ - endpoint: `/sliceaddview/api/read?_flt_0_created_by=${userId}`, + endpoint: `/sliceasync/api/read?_flt_0_created_by=${userId}`, }) .then(({ json }) => { const slices = {}; diff --git a/superset/connectors/druid/views.py b/superset/connectors/druid/views.py index 523bfcd08..5a2cea051 100644 --- a/superset/connectors/druid/views.py +++ b/superset/connectors/druid/views.py @@ -30,6 +30,7 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField from superset import app, appbuilder, db, security_manager from superset.connectors.base.views import DatasourceModelView from superset.connectors.connector_registry import ConnectorRegistry +from superset.constants import RouteMethod from superset.utils import core as utils from superset.views.base import ( BaseSupersetView, @@ -47,6 +48,7 @@ from . import models class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView): datamodel = SQLAInterface(models.DruidColumn) + include_route_methods = RouteMethod.RELATED_VIEW_SET list_title = _("Columns") show_title = _("Show Druid Column") @@ -133,6 +135,7 @@ class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView): class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): datamodel = SQLAInterface(models.DruidMetric) + include_route_methods = RouteMethod.RELATED_VIEW_SET list_title = _("Metrics") show_title = _("Show Druid Metric") @@ -185,7 +188,7 @@ class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin): datamodel = SQLAInterface(models.DruidCluster) - + include_route_methods = RouteMethod.CRUD_SET list_title = _("Druid Clusters") show_title = _("Show Druid Cluster") add_title = _("Add Druid Cluster") @@ -247,7 +250,7 @@ class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin): class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): datamodel = SQLAInterface(models.DruidDatasource) - + include_route_methods = RouteMethod.CRUD_SET list_title = _("Druid Datasources") show_title = _("Show Druid Datasource") add_title = _("Add Druid Datasource") diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index c51057a46..c63c4f8df 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -31,6 +31,7 @@ from wtforms.validators import Regexp from superset import appbuilder, db, security_manager from superset.connectors.base.views import DatasourceModelView +from superset.constants import RouteMethod from superset.utils import core as utils from superset.views.base import ( DatasourceFilter, @@ -48,6 +49,8 @@ logger = logging.getLogger(__name__) class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): datamodel = SQLAInterface(models.TableColumn) + # TODO TODO, review need for this on related_views + include_route_methods = RouteMethod.RELATED_VIEW_SET list_title = _("Columns") show_title = _("Show Column") @@ -164,6 +167,7 @@ class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): datamodel = SQLAInterface(models.SqlMetric) + include_route_methods = RouteMethod.RELATED_VIEW_SET list_title = _("Metrics") show_title = _("Show Metric") @@ -223,6 +227,7 @@ class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): datamodel = SQLAInterface(models.SqlaTable) + include_route_methods = RouteMethod.CRUD_SET list_title = _("Tables") show_title = _("Show Table") diff --git a/superset/constants.py b/superset/constants.py index f8472c54f..78b47e53f 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -19,3 +19,43 @@ # string to use when None values *need* to be converted to/from strings NULL_STRING = "" + + +class RouteMethod: # pylint: disable=too-few-public-methods + """ + Route methods are a FAB concept around ModelView and RestModelView + classes in FAB. Derivatives can define `include_route_method` and + `exclude_route_methods` class attribute as a set of methods that + will or won't get exposed. + + This class is a collection of static constants to reference common + route methods, namely the ones defined in the base classes in FAB + """ + + # ModelView specific + ACTION = "action" + ACTION_POST = "action_post" + ADD = "add" + API_CREATE = "api_create" + API_DELETE = "api_delete" + API_GET = "api_get" + API_READ = "api_read" + API_UPDATE = "api_update" + DELETE = "delete" + DOWNLOAD = "download" + EDIT = "edit" + LIST = "list" + SHOW = "show" + + # RestModelView specific + EXPORT = "export" + GET = "get" + GET_LIST = "get_list" + POST = "post" + PUT = "put" + RELATED = "related" + + # Commonly used sets + CRUD_SET = {ADD, LIST, EDIT, DELETE, ACTION_POST} + RELATED_VIEW_SET = {ADD, LIST, EDIT, DELETE} + REST_MODEL_VIEW_CRUD_SET = {DELETE, GET, GET_LIST, POST, PUT} diff --git a/superset/security/manager.py b/superset/security/manager.py index 8b436209e..f1b69d138 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -32,6 +32,7 @@ from flask_appbuilder.security.views import ( PermissionViewModelView, RoleModelView, UserModelView, + ViewMenuModelView, ) from flask_appbuilder.widgets import ListWidget from sqlalchemy import or_ @@ -40,6 +41,7 @@ from sqlalchemy.orm.mapper import Mapper from superset import sql_parse from superset.connectors.connector_registry import ConnectorRegistry +from superset.constants import RouteMethod from superset.exceptions import SupersetSecurityException from superset.utils.core import DatasourceName @@ -76,8 +78,16 @@ RoleModelView.list_widget = SupersetRoleListWidget PermissionViewModelView.list_widget = SupersetSecurityListWidget PermissionModelView.list_widget = SupersetSecurityListWidget +# Limiting routes on FAB model views +UserModelView.include_route_methods = RouteMethod.CRUD_SET | {"userinfo"} +RoleModelView.include_route_methods = RouteMethod.CRUD_SET +PermissionViewModelView.include_route_methods = {RouteMethod.LIST} +PermissionModelView.include_route_methods = {RouteMethod.LIST} +ViewMenuModelView.include_route_methods = {RouteMethod.LIST} + class SupersetSecurityManager(SecurityManager): + userstatschartview = None READ_ONLY_MODEL_VIEWS = {"DatabaseAsync", "DatabaseView", "DruidClusterModelView"} USER_MODEL_VIEWS = { diff --git a/superset/views/annotations.py b/superset/views/annotations.py index 33fdb3825..5ba975050 100644 --- a/superset/views/annotations.py +++ b/superset/views/annotations.py @@ -18,6 +18,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_babel import lazy_gettext as _ from wtforms.validators import StopValidation +from superset.constants import RouteMethod from superset.models.annotations import Annotation, AnnotationLayer from .base import DeleteMixin, SupersetModelView @@ -45,6 +46,7 @@ class AnnotationModelView( SupersetModelView, DeleteMixin ): # pylint: disable=too-many-ancestors datamodel = SQLAInterface(Annotation) + include_route_methods = RouteMethod.CRUD_SET list_title = _("List Annotation") show_title = _("Show Annotation") @@ -93,6 +95,7 @@ class AnnotationLayerModelView( SupersetModelView, DeleteMixin ): # pylint: disable=too-many-ancestors datamodel = SQLAInterface(AnnotationLayer) + include_route_methods = RouteMethod.CRUD_SET list_title = _("List Annotation Layer") show_title = _("Show Annotation Layer") diff --git a/superset/views/base_api.py b/superset/views/base_api.py index a2ee1d408..976f5b499 100644 --- a/superset/views/base_api.py +++ b/superset/views/base_api.py @@ -63,6 +63,17 @@ class BaseSupersetModelRestApi(ModelRestApi): """ logger = logging.getLogger(__name__) + method_permission_name = { + "get_list": "list", + "get": "show", + "export": "mulexport", + "post": "add", + "put": "edit", + "delete": "delete", + "bulk_delete": "delete", + "info": "list", + "related": "list", + } order_rel_fields: Dict[str, Tuple[str, str]] = {} """ diff --git a/superset/views/chart/api.py b/superset/views/chart/api.py index 511b62ae9..47c7d7fb3 100644 --- a/superset/views/chart/api.py +++ b/superset/views/chart/api.py @@ -134,15 +134,6 @@ class ChartRestApi(SliceMixin, BaseOwnedModelRestApi): allow_browser_login = True class_permission_name = "SliceModelView" - method_permission_name = { - "get_list": "list", - "get": "show", - "post": "add", - "put": "edit", - "delete": "delete", - "info": "list", - "related": "list", - } show_columns = [ "slice_name", "description", diff --git a/superset/views/chart/views.py b/superset/views/chart/views.py index d6a0c7ed8..a493600b9 100644 --- a/superset/views/chart/views.py +++ b/superset/views/chart/views.py @@ -22,6 +22,7 @@ from flask_babel import lazy_gettext as _ from superset import db from superset.connectors.connector_registry import ConnectorRegistry +from superset.constants import RouteMethod from superset.models.slice import Slice from superset.utils import core as utils from superset.views.base import check_ownership, DeleteMixin, SupersetModelView @@ -33,6 +34,11 @@ class SliceModelView( ): # pylint: disable=too-many-ancestors route_base = "/chart" datamodel = SQLAInterface(Slice) + include_route_methods = RouteMethod.CRUD_SET | { + RouteMethod.DOWNLOAD, + RouteMethod.API_READ, + RouteMethod.API_DELETE, + } def pre_add(self, item): utils.validate_json(item.params) @@ -61,36 +67,27 @@ class SliceModelView( class SliceAsync(SliceModelView): # pylint: disable=too-many-ancestors route_base = "/sliceasync" - list_columns = [ - "id", - "slice_link", - "viz_type", - "slice_name", - "creator", - "modified", - "icons", - "changed_on_humanized", - ] - label_columns = {"icons": " ", "slice_link": _("Chart")} + include_route_methods = {RouteMethod.API_READ} - -class SliceAddView(SliceModelView): # pylint: disable=too-many-ancestors - route_base = "/sliceaddview" list_columns = [ - "id", - "slice_name", - "slice_url", - "edit_url", - "viz_type", - "params", - "description", - "description_markeddown", - "datasource_id", - "datasource_type", - "datasource_name_text", - "datasource_link", - "owners", - "modified", "changed_on", "changed_on_humanized", + "creator", + "datasource_id", + "datasource_link", + "datasource_name_text", + "datasource_type", + "description", + "description_markeddown", + "edit_url", + "icons", + "id", + "modified", + "owners", + "params", + "slice_link", + "slice_name", + "slice_url", + "viz_type", ] + label_columns = {"icons": " ", "slice_link": _("Chart")} diff --git a/superset/views/core.py b/superset/views/core.py index d96d67c79..7957572d5 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -70,6 +70,7 @@ from superset import ( ) from superset.connectors.connector_registry import ConnectorRegistry from superset.connectors.sqla.models import AnnotationDatasource +from superset.constants import RouteMethod from superset.exceptions import ( DatabaseNotFound, SupersetException, @@ -250,6 +251,7 @@ def _deserialize_results_payload( class AccessRequestsModelView(SupersetModelView, DeleteMixin): datamodel = SQLAInterface(DAR) + include_route_methods = RouteMethod.CRUD_SET list_columns = [ "username", "user_roles", @@ -2816,6 +2818,7 @@ class Superset(BaseSupersetView): class CssTemplateModelView(SupersetModelView, DeleteMixin): datamodel = SQLAInterface(models.CssTemplate) + include_route_methods = RouteMethod.CRUD_SET list_title = _("CSS Templates") show_title = _("Show CSS Template") @@ -2829,6 +2832,7 @@ class CssTemplateModelView(SupersetModelView, DeleteMixin): class CssTemplateAsyncModelView(CssTemplateModelView): + include_route_methods = {RouteMethod.API_READ} list_columns = ["template_name", "css"] @@ -2845,16 +2849,3 @@ def apply_http_headers(response: Response): if k not in response.headers: response.headers[k] = v return response - - -@app.route('/') -def panoramix(url): - return redirect(request.full_path.replace("panoramix", "superset")) - - -@app.route('/') -def caravel(url): - return redirect(request.full_path.replace("caravel", "superset")) - - -# --------------------------------------------------------------------- diff --git a/superset/views/dashboard/api.py b/superset/views/dashboard/api.py index 857f99ebd..80cac5569 100644 --- a/superset/views/dashboard/api.py +++ b/superset/views/dashboard/api.py @@ -27,6 +27,7 @@ from marshmallow import fields, post_load, pre_load, Schema, ValidationError from marshmallow.validate import Length from sqlalchemy.exc import SQLAlchemyError +from superset.constants import RouteMethod from superset.exceptions import SupersetException, SupersetSecurityException from superset.models.dashboard import Dashboard from superset.utils import core as utils @@ -130,22 +131,15 @@ get_export_ids_schema = {"type": "array", "items": {"type": "integer"}} class DashboardRestApi(DashboardMixin, BaseOwnedModelRestApi): datamodel = SQLAInterface(Dashboard) - + include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | { + RouteMethod.EXPORT, + RouteMethod.RELATED, + "bulk_delete", # not using RouteMethod since locally defined + } resource_name = "dashboard" allow_browser_login = True class_permission_name = "DashboardModelView" - method_permission_name = { - "get_list": "list", - "get": "show", - "export": "mulexport", - "post": "add", - "put": "edit", - "delete": "delete", - "bulk_delete": "delete", - "info": "list", - "related": "list", - } show_columns = [ "dashboard_title", "slug", diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py index 236f426a4..69c37e5e8 100644 --- a/superset/views/dashboard/views.py +++ b/superset/views/dashboard/views.py @@ -26,6 +26,7 @@ from flask_babel import gettext as __, lazy_gettext as _ import superset.models.core as models from superset import db, event_logger +from superset.constants import RouteMethod from superset.utils import core as utils from ..base import ( @@ -45,6 +46,13 @@ class DashboardModelView( ): # pylint: disable=too-many-ancestors route_base = "/dashboard" datamodel = SQLAInterface(models.Dashboard) + # TODO disable api_read and api_delete (used by cypress) + # once we move to ChartRestModelApi + include_route_methods = RouteMethod.CRUD_SET | { + RouteMethod.API_READ, + RouteMethod.API_DELETE, + "download_dashboards", + } @has_access @expose("/list/") @@ -119,6 +127,8 @@ class Dashboard(BaseSupersetView): class DashboardModelViewAsync(DashboardModelView): # pylint: disable=too-many-ancestors route_base = "/dashboardasync" + include_route_methods = {RouteMethod.API_READ} + list_columns = [ "id", "dashboard_link", @@ -135,18 +145,3 @@ class DashboardModelViewAsync(DashboardModelView): # pylint: disable=too-many-a "creator": _("Creator"), "modified": _("Modified"), } - - -class DashboardAddView(DashboardModelView): # pylint: disable=too-many-ancestors - route_base = "/dashboardaddview" - list_columns = [ - "id", - "dashboard_link", - "creator", - "modified", - "dashboard_title", - "changed_on", - "url", - "changed_by_name", - ] - show_columns = list(set(DashboardModelView.edit_columns + list_columns)) diff --git a/superset/views/database/api.py b/superset/views/database/api.py index 0c1b688a9..72d2d093a 100644 --- a/superset/views/database/api.py +++ b/superset/views/database/api.py @@ -14,27 +14,20 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from flask_appbuilder import ModelRestApi from flask_appbuilder.models.sqla.interface import SQLAInterface import superset.models.core as models +from superset.views.base_api import BaseSupersetModelRestApi from .mixins import DatabaseFilter, DatabaseMixin from .validators import sqlalchemy_uri_validator -class DatabaseRestApi(DatabaseMixin, ModelRestApi): +class DatabaseRestApi(DatabaseMixin, BaseSupersetModelRestApi): datamodel = SQLAInterface(models.Database) + include_route_methods = {"get_list"} - class_permission_name = "DatabaseAsync" - method_permission_name = { - "get_list": "list", - "get": "show", - "post": "add", - "put": "edit", - "delete": "delete", - "info": "list", - } + class_permission_name = "DatabaseView" resource_name = "database" allow_browser_login = True base_filters = [["id", DatabaseFilter, lambda: []]] diff --git a/superset/views/database/views.py b/superset/views/database/views.py index 19620e1d1..9a28715f6 100644 --- a/superset/views/database/views.py +++ b/superset/views/database/views.py @@ -27,6 +27,7 @@ from wtforms.validators import ValidationError import superset.models.core as models from superset import app, db from superset.connectors.sqla.models import SqlaTable +from superset.constants import RouteMethod from superset.utils import core as utils from superset.views.base import DeleteMixin, SupersetModelView, YamlExportMixin @@ -49,6 +50,7 @@ class DatabaseView( DatabaseMixin, SupersetModelView, DeleteMixin, YamlExportMixin ): # pylint: disable=too-many-ancestors datamodel = SQLAInterface(models.Database) + include_route_methods = RouteMethod.CRUD_SET add_template = "superset/models/database/add.html" edit_template = "superset/models/database/edit.html" @@ -157,23 +159,3 @@ class CsvToDatabaseView(SimpleFormView): flash(message, "info") stats_logger.incr("successful_csv_upload") return redirect("/tablemodelview/list/") - - -class DatabaseTablesAsync(DatabaseView): # pylint: disable=too-many-ancestors - list_columns = ["id", "all_table_names_in_database", "all_schema_names"] - - -class DatabaseAsync(DatabaseView): # pylint: disable=too-many-ancestors - list_columns = [ - "id", - "database_name", - "expose_in_sqllab", - "allow_ctas", - "force_ctas_schema", - "allow_run_async", - "allow_dml", - "allow_multi_schema_metadata_fetch", - "allow_csv_upload", - "allows_subquery", - "backend", - ] diff --git a/superset/views/log/api.py b/superset/views/log/api.py index c26e0917d..cc9424ef7 100644 --- a/superset/views/log/api.py +++ b/superset/views/log/api.py @@ -14,26 +14,18 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from flask_appbuilder import ModelRestApi from flask_appbuilder.models.sqla.interface import SQLAInterface import superset.models.core as models +from superset.views.base_api import BaseSupersetModelRestApi from . import LogMixin -class LogRestApi(LogMixin, ModelRestApi): +class LogRestApi(LogMixin, BaseSupersetModelRestApi): datamodel = SQLAInterface(models.Log) class_permission_name = "LogModelView" - method_permission_name = { - "get_list": "list", - "get": "show", - "post": "add", - "put": "edit", - "delete": "delete", - "info": "list", - } resource_name = "log" allow_browser_login = True list_columns = ("user.username", "action", "dttm") diff --git a/superset/views/log/views.py b/superset/views/log/views.py index a9f3bd016..7139d3485 100644 --- a/superset/views/log/views.py +++ b/superset/views/log/views.py @@ -17,6 +17,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface import superset.models.core as models +from superset.constants import RouteMethod from superset.views.base import SupersetModelView from . import LogMixin @@ -24,3 +25,4 @@ from . import LogMixin class LogModelView(LogMixin, SupersetModelView): # pylint: disable=too-many-ancestors datamodel = SQLAInterface(models.Log) + include_route_methods = {RouteMethod.LIST, RouteMethod.SHOW} diff --git a/superset/views/schedules.py b/superset/views/schedules.py index c1f690d81..ad7861e75 100644 --- a/superset/views/schedules.py +++ b/superset/views/schedules.py @@ -27,6 +27,7 @@ from flask_babel import lazy_gettext as _ from wtforms import BooleanField, StringField from superset import db, security_manager +from superset.constants import RouteMethod from superset.exceptions import SupersetException from superset.models.dashboard import Dashboard from superset.models.schedules import ( @@ -45,6 +46,7 @@ from .base import DeleteMixin, SupersetModelView class EmailScheduleView( SupersetModelView, DeleteMixin ): # pylint: disable=too-many-ancestors + include_route_methods = RouteMethod.CRUD_SET _extra_data = {"test_email": False, "test_email_recipients": None} schedule_type: Optional[Type] = None schedule_type_model: Optional[Type] = None diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py index e53141868..5414c1595 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -25,6 +25,7 @@ from flask_babel import lazy_gettext as _ from flask_sqlalchemy import BaseQuery from superset import db, get_feature_flags, security_manager +from superset.constants import RouteMethod from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState from superset.utils import core as utils @@ -52,6 +53,7 @@ class QueryFilter(BaseFilter): # pylint: disable=too-few-public-methods class QueryView(SupersetModelView): datamodel = SQLAInterface(Query) + include_route_methods = {RouteMethod.SHOW, RouteMethod.LIST, RouteMethod.API_READ} list_title = _("List Query") show_title = _("Show Query") @@ -75,6 +77,7 @@ class SavedQueryView( SupersetModelView, DeleteMixin ): # pylint: disable=too-many-ancestors datamodel = SQLAInterface(SavedQuery) + include_route_methods = RouteMethod.CRUD_SET list_title = _("List Saved Query") show_title = _("Show Saved Query") @@ -143,6 +146,12 @@ class SavedQueryView( class SavedQueryViewApi(SavedQueryView): # pylint: disable=too-many-ancestors + include_route_methods = { + RouteMethod.API_READ, + RouteMethod.API_CREATE, + RouteMethod.API_UPDATE, + RouteMethod.API_GET, + } list_columns = [ "id", "label", diff --git a/tests/core_tests.py b/tests/core_tests.py index 060b29255..cc53141b7 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -345,7 +345,7 @@ class CoreTests(SupersetTestCase): def test_get_user_slices(self): self.login(username="admin") userid = security_manager.find_user("admin").id - url = "/sliceaddview/api/read?_flt_0_created_by={}".format(userid) + url = f"/sliceasync/api/read?_flt_0_created_by={userid}" resp = self.client.get(url) self.assertEqual(resp.status_code, 200) diff --git a/tests/import_export_tests.py b/tests/import_export_tests.py index b932bf031..bf5f6dd55 100644 --- a/tests/import_export_tests.py +++ b/tests/import_export_tests.py @@ -235,9 +235,8 @@ class ImportExportTests(SupersetTestCase): def test_export_1_dashboard(self): self.login("admin") birth_dash = self.get_dash_by_slug("births") - export_dash_url = "/dashboard/export_dashboards_form?id={}&action=go".format( - birth_dash.id - ) + id_ = birth_dash.id + export_dash_url = f"/dashboard/export_dashboards_form?id={id_}&action=go" resp = self.client.get(export_dash_url) exported_dashboards = json.loads( resp.data.decode("utf-8"), object_hook=decode_dashboards @@ -247,7 +246,7 @@ class ImportExportTests(SupersetTestCase): self.assert_only_exported_slc_fields(birth_dash, exported_dashboards[0]) self.assert_dash_equals(birth_dash, exported_dashboards[0]) self.assertEqual( - birth_dash.id, + id_, json.loads( exported_dashboards[0].json_metadata, object_hook=decode_dashboards )["remote_id"], diff --git a/tests/security_tests.py b/tests/security_tests.py index afaad3866..f16f41103 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -486,14 +486,6 @@ class RolePermissionTests(SupersetTestCase): example_db.expose_in_sqllab = True session.commit() - OLD_FLASK_GET_SQL_DBS_REQUEST = ( - "databaseasync/api/read?_flt_0_expose_in_sqllab=1&" - "_oc_DatabaseAsync=database_name&_od_DatabaseAsync=asc" - ) - self.login(username="gamma") - databases_json = self.client.get(OLD_FLASK_GET_SQL_DBS_REQUEST).json - self.assertEquals(databases_json["count"], 1) - arguments = { "keys": ["none"], "filters": [{"col": "expose_in_sqllab", "opr": "eq", "value": True}], @@ -509,18 +501,15 @@ class RolePermissionTests(SupersetTestCase): self.logout() def assert_can_read(self, view_menu, permissions_set): - self.assertIn(("can_show", view_menu), permissions_set) self.assertIn(("can_list", view_menu), permissions_set) def assert_can_write(self, view_menu, permissions_set): self.assertIn(("can_add", view_menu), permissions_set) - self.assertIn(("can_download", view_menu), permissions_set) self.assertIn(("can_delete", view_menu), permissions_set) self.assertIn(("can_edit", view_menu), permissions_set) def assert_cannot_write(self, view_menu, permissions_set): self.assertNotIn(("can_add", view_menu), permissions_set) - self.assertNotIn(("can_download", view_menu), permissions_set) self.assertNotIn(("can_delete", view_menu), permissions_set) self.assertNotIn(("can_edit", view_menu), permissions_set) self.assertNotIn(("can_save", view_menu), permissions_set) @@ -530,7 +519,6 @@ class RolePermissionTests(SupersetTestCase): self.assert_can_write(view_menu, permissions_set) def assert_can_gamma(self, perm_set): - self.assert_can_read("DatabaseAsync", perm_set) self.assert_can_read("TableModelView", perm_set) # make sure that user can create slices and dashboards @@ -554,8 +542,6 @@ class RolePermissionTests(SupersetTestCase): self.assertIn(("can_userinfo", "UserDBModelView"), perm_set) def assert_can_alpha(self, perm_set): - self.assert_can_all("SqlMetricInlineView", perm_set) - self.assert_can_all("TableColumnInlineView", perm_set) self.assert_can_all("TableModelView", perm_set) self.assertIn(("all_datasource_access", "all_datasource_access"), perm_set) @@ -569,7 +555,6 @@ class RolePermissionTests(SupersetTestCase): self.assert_cannot_write("UserDBModelView", perm_set) def assert_can_admin(self, perm_set): - self.assert_can_read("DatabaseAsync", perm_set) self.assert_can_all("DatabaseView", perm_set) self.assert_can_all("RoleModelView", perm_set) self.assert_can_all("UserDBModelView", perm_set) @@ -583,7 +568,7 @@ class RolePermissionTests(SupersetTestCase): def test_is_admin_only(self): self.assertFalse( security_manager._is_admin_only( - security_manager.find_permission_view_menu("can_show", "TableModelView") + security_manager.find_permission_view_menu("can_list", "TableModelView") ) ) self.assertFalse( @@ -603,7 +588,7 @@ class RolePermissionTests(SupersetTestCase): self.assertTrue( security_manager._is_admin_only( security_manager.find_permission_view_menu( - "can_show", "AccessRequestsModelView" + "can_list", "AccessRequestsModelView" ) ) ) @@ -626,7 +611,7 @@ class RolePermissionTests(SupersetTestCase): def test_is_alpha_only(self): self.assertFalse( security_manager._is_alpha_only( - security_manager.find_permission_view_menu("can_show", "TableModelView") + security_manager.find_permission_view_menu("can_list", "TableModelView") ) ) @@ -644,13 +629,6 @@ class RolePermissionTests(SupersetTestCase): ) ) ) - self.assertTrue( - security_manager._is_alpha_only( - security_manager.find_permission_view_menu( - "can_edit", "SqlMetricInlineView" - ) - ) - ) self.assertTrue( security_manager._is_alpha_only( security_manager.find_permission_view_menu( @@ -662,7 +640,7 @@ class RolePermissionTests(SupersetTestCase): def test_is_gamma_pvm(self): self.assertTrue( security_manager._is_gamma_pvm( - security_manager.find_permission_view_menu("can_show", "TableModelView") + security_manager.find_permission_view_menu("can_list", "TableModelView") ) ) @@ -674,9 +652,10 @@ class RolePermissionTests(SupersetTestCase): SupersetTestCase.is_module_installed("pydruid"), "pydruid not installed" ) def test_alpha_permissions(self): - self.assert_can_gamma(get_perm_tuples("Alpha")) - self.assert_can_alpha(get_perm_tuples("Alpha")) - self.assert_cannot_alpha(get_perm_tuples("Alpha")) + alpha_perm_tuples = get_perm_tuples("Alpha") + self.assert_can_gamma(alpha_perm_tuples) + self.assert_can_alpha(alpha_perm_tuples) + self.assert_cannot_alpha(alpha_perm_tuples) @unittest.skipUnless( SupersetTestCase.is_module_installed("pydruid"), "pydruid not installed" @@ -703,18 +682,15 @@ class RolePermissionTests(SupersetTestCase): def test_gamma_permissions(self): def assert_can_read(view_menu): - self.assertIn(("can_show", view_menu), gamma_perm_set) self.assertIn(("can_list", view_menu), gamma_perm_set) def assert_can_write(view_menu): self.assertIn(("can_add", view_menu), gamma_perm_set) - self.assertIn(("can_download", view_menu), gamma_perm_set) self.assertIn(("can_delete", view_menu), gamma_perm_set) self.assertIn(("can_edit", view_menu), gamma_perm_set) def assert_cannot_write(view_menu): self.assertNotIn(("can_add", view_menu), gamma_perm_set) - self.assertNotIn(("can_download", view_menu), gamma_perm_set) self.assertNotIn(("can_delete", view_menu), gamma_perm_set) self.assertNotIn(("can_edit", view_menu), gamma_perm_set) self.assertNotIn(("can_save", view_menu), gamma_perm_set)