You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2021/01/31 07:18:42 UTC

[superset] branch master updated: feat(dashboard-rbac): dashboard lists (#12680)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9a7fba8  feat(dashboard-rbac): dashboard lists (#12680)
9a7fba8 is described below

commit 9a7fba810e5bc3a94746117937163c1bfaea19ee
Author: Amit Miran <47...@users.noreply.github.com>
AuthorDate: Sun Jan 31 09:17:46 2021 +0200

    feat(dashboard-rbac): dashboard lists (#12680)
---
 superset/config.py                                 |   1 +
 superset/dashboards/filters.py                     |  35 ++-
 ...cdd12658_add_roles_relationship_to_dashboard.py |  42 +++
 superset/models/dashboard.py                       |  11 +-
 superset/views/dashboard/mixin.py                  |   8 +
 tests/base_tests.py                                |  26 +-
 tests/dashboard_tests.py                           |  18 --
 tests/dashboards/base_case.py                      | 114 ++++++++
 tests/dashboards/consts.py                         |  43 +++
 tests/dashboards/dashboard_test_utils.py           | 121 ++++++++
 tests/dashboards/security/__init__.py              |  16 ++
 tests/dashboards/security/base_case.py             |  86 ++++++
 .../dashboards/security/security_dataset_tests.py  | 241 ++++++++++++++++
 tests/dashboards/security/security_rbac_tests.py   | 308 +++++++++++++++++++++
 tests/dashboards/superset_factory_util.py          | 305 ++++++++++++++++++++
 15 files changed, 1345 insertions(+), 30 deletions(-)

diff --git a/superset/config.py b/superset/config.py
index 59a54fe..d7c8d5e 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -343,6 +343,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
     "ALERT_REPORTS": False,
     # Enable experimental feature to search for other dashboards
     "OMNIBAR": False,
+    "DASHBOARD_RBAC": False,
 }
 
 # Set the default view to card/grid view if thumbnail support is enabled.
diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py
index a2421ab..7ca466e 100644
--- a/superset/dashboards/filters.py
+++ b/superset/dashboards/filters.py
@@ -16,15 +16,16 @@
 # under the License.
 from typing import Any
 
+from flask_appbuilder.security.sqla.models import Role
 from flask_babel import lazy_gettext as _
 from sqlalchemy import and_, or_
 from sqlalchemy.orm.query import Query
 
-from superset import db, security_manager
+from superset import db, is_feature_enabled, security_manager
 from superset.models.core import FavStar
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
-from superset.views.base import BaseFilter, is_user_admin
+from superset.views.base import BaseFilter, get_user_roles, is_user_admin
 from superset.views.base_api import BaseFavoriteFilter
 
 
@@ -74,12 +75,19 @@ class DashboardFilter(BaseFilter):  # pylint: disable=too-few-public-methods
 
         datasource_perms = security_manager.user_view_menu_names("datasource_access")
         schema_perms = security_manager.user_view_menu_names("schema_access")
-        published_dash_query = (
+
+        is_rbac_disabled_filter = []
+        dashboard_has_roles = Dashboard.roles.any()
+        if is_feature_enabled("DASHBOARD_RBAC"):
+            is_rbac_disabled_filter.append(~dashboard_has_roles)
+
+        datasource_perm_query = (
             db.session.query(Dashboard.id)
             .join(Dashboard.slices)
             .filter(
                 and_(
-                    Dashboard.published == True,  # pylint: disable=singleton-comparison
+                    Dashboard.published.is_(True),
+                    *is_rbac_disabled_filter,
                     or_(
                         Slice.perm.in_(datasource_perms),
                         Slice.schema_perm.in_(schema_perms),
@@ -104,11 +112,28 @@ class DashboardFilter(BaseFilter):  # pylint: disable=too-few-public-methods
             )
         )
 
+        dashboard_rbac_or_filters = []
+        if is_feature_enabled("DASHBOARD_RBAC"):
+            roles_based_query = (
+                db.session.query(Dashboard.id)
+                .join(Dashboard.roles)
+                .filter(
+                    and_(
+                        Dashboard.published.is_(True),
+                        dashboard_has_roles,
+                        Role.id.in_([x.id for x in get_user_roles()]),
+                    ),
+                )
+            )
+
+            dashboard_rbac_or_filters.append(Dashboard.id.in_(roles_based_query))
+
         query = query.filter(
             or_(
                 Dashboard.id.in_(owner_ids_query),
-                Dashboard.id.in_(published_dash_query),
+                Dashboard.id.in_(datasource_perm_query),
                 Dashboard.id.in_(users_favorite_dash_query),
+                *dashboard_rbac_or_filters,
             )
         )
 
diff --git a/superset/migrations/versions/11ccdd12658_add_roles_relationship_to_dashboard.py b/superset/migrations/versions/11ccdd12658_add_roles_relationship_to_dashboard.py
new file mode 100644
index 0000000..b5576ca
--- /dev/null
+++ b/superset/migrations/versions/11ccdd12658_add_roles_relationship_to_dashboard.py
@@ -0,0 +1,42 @@
+# 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.
+"""add roles relationship to dashboard
+Revision ID: e11ccdd12658
+Revises: 260bf0649a77
+Create Date: 2021-01-14 19:12:43.406230
+"""
+# revision identifiers, used by Alembic.
+revision = "e11ccdd12658"
+down_revision = "260bf0649a77"
+import sqlalchemy as sa
+from alembic import op
+
+
+def upgrade():
+    op.create_table(
+        "dashboard_roles",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("role_id", sa.Integer(), nullable=False),
+        sa.Column("dashboard_id", sa.Integer(), nullable=True),
+        sa.ForeignKeyConstraint(["dashboard_id"], ["dashboards.id"]),
+        sa.ForeignKeyConstraint(["role_id"], ["ab_role.id"]),
+        sa.PrimaryKeyConstraint("id"),
+    )
+
+
+def downgrade():
+    op.drop_table("dashboard_roles")
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index d7c2a69..3606da8 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -115,6 +115,15 @@ dashboard_user = Table(
 )
 
 
+DashboardRoles = Table(
+    "dashboard_roles",
+    metadata,
+    Column("id", Integer, primary_key=True),
+    Column("dashboard_id", Integer, ForeignKey("dashboards.id"), nullable=False),
+    Column("role_id", Integer, ForeignKey("ab_role.id"), nullable=False),
+)
+
+
 class Dashboard(  # pylint: disable=too-many-instance-attributes
     Model, AuditMixinNullable, ImportExportMixin
 ):
@@ -132,7 +141,7 @@ class Dashboard(  # pylint: disable=too-many-instance-attributes
     slices = relationship(Slice, secondary=dashboard_slices, backref="dashboards")
     owners = relationship(security_manager.user_model, secondary=dashboard_user)
     published = Column(Boolean, default=False)
-
+    roles = relationship(security_manager.role_model, secondary=DashboardRoles)
     export_fields = [
         "dashboard_title",
         "position_json",
diff --git a/superset/views/dashboard/mixin.py b/superset/views/dashboard/mixin.py
index 89472ac..273cfdf 100644
--- a/superset/views/dashboard/mixin.py
+++ b/superset/views/dashboard/mixin.py
@@ -33,6 +33,7 @@ class DashboardMixin:  # pylint: disable=too-few-public-methods
         "dashboard_title",
         "slug",
         "owners",
+        "roles",
         "position_json",
         "css",
         "json_metadata",
@@ -62,6 +63,12 @@ class DashboardMixin:  # pylint: disable=too-few-public-methods
             "want to alter specific parameters."
         ),
         "owners": _("Owners is a list of users who can alter the dashboard."),
+        "roles": _(
+            "Roles is a list which defines access to the dashboard. "
+            "These roles are always applied in addition to restrictions on dataset "
+            "level access. "
+            "If no roles defined then the dashboard is available to all roles."
+        ),
         "published": _(
             "Determines whether or not this dashboard is "
             "visible in the list of all dashboards"
@@ -74,6 +81,7 @@ class DashboardMixin:  # pylint: disable=too-few-public-methods
         "slug": _("Slug"),
         "charts": _("Charts"),
         "owners": _("Owners"),
+        "roles": _("Roles"),
         "published": _("Published"),
         "creator": _("Creator"),
         "modified": _("Modified"),
diff --git a/tests/base_tests.py b/tests/base_tests.py
index 81e218b..a5fd9d5 100644
--- a/tests/base_tests.py
+++ b/tests/base_tests.py
@@ -111,7 +111,6 @@ def logged_in_admin():
 
 
 class SupersetTestCase(TestCase):
-
     default_schema_backend_map = {
         "sqlite": "main",
         "mysql": "superset",
@@ -135,7 +134,9 @@ class SupersetTestCase(TestCase):
         )
 
     @staticmethod
-    def create_user_with_roles(username: str, roles: List[str]):
+    def create_user_with_roles(
+        username: str, roles: List[str], should_create_roles: bool = False
+    ):
         user_to_create = security_manager.find_user(username)
         if not user_to_create:
             security_manager.add_user(
@@ -149,7 +150,12 @@ class SupersetTestCase(TestCase):
             db.session.commit()
             user_to_create = security_manager.find_user(username)
             assert user_to_create
-        user_to_create.roles = [security_manager.find_role(r) for r in roles]
+        user_to_create.roles = []
+        for chosen_user_role in roles:
+            if should_create_roles:
+                ## copy role from gamma but without data permissions
+                security_manager.copy_role("Gamma", chosen_user_role, merge=False)
+            user_to_create.roles.append(security_manager.find_role(chosen_user_role))
         db.session.commit()
         return user_to_create
 
@@ -290,7 +296,11 @@ class SupersetTestCase(TestCase):
         self.client.get("/logout/", follow_redirects=True)
 
     def grant_public_access_to_table(self, table):
-        public_role = security_manager.find_role("Public")
+        role_name = "Public"
+        self.grant_role_access_to_table(table, role_name)
+
+    def grant_role_access_to_table(self, table, role_name):
+        role = security_manager.find_role(role_name)
         perms = db.session.query(ab_models.PermissionView).all()
         for perm in perms:
             if (
@@ -298,10 +308,14 @@ class SupersetTestCase(TestCase):
                 and perm.view_menu
                 and table.perm in perm.view_menu.name
             ):
-                security_manager.add_permission_role(public_role, perm)
+                security_manager.add_permission_role(role, perm)
 
     def revoke_public_access_to_table(self, table):
-        public_role = security_manager.find_role("Public")
+        role_name = "Public"
+        self.revoke_role_access_to_table(role_name, table)
+
+    def revoke_role_access_to_table(self, role_name, table):
+        public_role = security_manager.find_role(role_name)
         perms = db.session.query(ab_models.PermissionView).all()
         for perm in perms:
             if (
diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py
index be3e0a9..9780b16 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -453,24 +453,6 @@ class TestDashboard(SupersetTestCase):
         db.session.commit()
         self.test_save_dash("alpha")
 
-    def test_owners_can_view_empty_dashboard(self):
-        dash = db.session.query(Dashboard).filter_by(slug="empty_dashboard").first()
-        if not dash:
-            dash = Dashboard()
-            dash.dashboard_title = "Empty Dashboard"
-            dash.slug = "empty_dashboard"
-        else:
-            dash.slices = []
-            dash.owners = []
-        db.session.merge(dash)
-        db.session.commit()
-
-        gamma_user = security_manager.find_user("gamma")
-        self.login(gamma_user.username)
-
-        resp = self.get_resp("/api/v1/dashboard/")
-        self.assertNotIn("/superset/dashboard/empty_dashboard/", resp)
-
     @pytest.mark.usefixtures("load_energy_table_with_slice", "load_dashboard")
     def test_users_can_view_published_dashboard(self):
         resp = self.get_resp("/api/v1/dashboard/")
diff --git a/tests/dashboards/base_case.py b/tests/dashboards/base_case.py
new file mode 100644
index 0000000..42cd87b
--- /dev/null
+++ b/tests/dashboards/base_case.py
@@ -0,0 +1,114 @@
+# 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.
+import json
+from typing import Any, Dict, Union
+
+import prison
+from flask import Response
+
+from superset import app, security_manager
+from tests.base_tests import SupersetTestCase
+from tests.dashboards.consts import *
+from tests.dashboards.dashboard_test_utils import build_save_dash_parts
+from tests.dashboards.superset_factory_util import delete_all_inserted_objects
+
+
+class DashboardTestCase(SupersetTestCase):
+    def get_dashboard_via_api_by_id(self, dashboard_id: int) -> Response:
+        uri = DASHBOARD_API_URL_FORMAT.format(dashboard_id)
+        return self.get_assert_metric(uri, "get")
+
+    def get_dashboard_view_response(self, dashboard_to_access) -> Response:
+        return self.client.get(dashboard_to_access.url)
+
+    def get_dashboard_api_response(self, dashboard_to_access) -> Response:
+        return self.client.get(DASHBOARD_API_URL_FORMAT.format(dashboard_to_access.id))
+
+    def get_dashboards_list_response(self) -> Response:
+        return self.client.get(GET_DASHBOARDS_LIST_VIEW)
+
+    def get_dashboards_api_response(self) -> Response:
+        return self.client.get(DASHBOARDS_API_URL)
+
+    def save_dashboard_via_view(
+        self, dashboard_id: Union[str, int], dashboard_data: Dict[str, Any]
+    ) -> Response:
+        save_dash_url = SAVE_DASHBOARD_URL_FORMAT.format(dashboard_id)
+        return self.get_resp(save_dash_url, data=dict(data=json.dumps(dashboard_data)))
+
+    def save_dashboard(
+        self, dashboard_id: Union[str, int], dashboard_data: Dict[str, Any]
+    ) -> Response:
+        return self.save_dashboard_via_view(dashboard_id, dashboard_data)
+
+    def delete_dashboard_via_view(self, dashboard_id: int) -> Response:
+        delete_dashboard_url = DELETE_DASHBOARD_VIEW_URL_FORMAT.format(dashboard_id)
+        return self.get_resp(delete_dashboard_url, {})
+
+    def delete_dashboard_via_api(self, dashboard_id):
+        uri = DASHBOARD_API_URL_FORMAT.format(dashboard_id)
+        return self.delete_assert_metric(uri, "delete")
+
+    def bulk_delete_dashboard_via_api(self, dashboard_ids):
+        uri = DASHBOARDS_API_URL_WITH_QUERY_FORMAT.format(prison.dumps(dashboard_ids))
+        return self.delete_assert_metric(uri, "bulk_delete")
+
+    def delete_dashboard(self, dashboard_id: int) -> Response:
+        return self.delete_dashboard_via_view(dashboard_id)
+
+    def assert_permission_was_created(self, dashboard):
+        view_menu = security_manager.find_view_menu(dashboard.view_name)
+        self.assertIsNotNone(view_menu)
+        self.assertEqual(len(security_manager.find_permissions_view_menu(view_menu)), 1)
+
+    def assert_permission_kept_and_changed(self, updated_dashboard, excepted_view_id):
+        view_menu_after_title_changed = security_manager.find_view_menu(
+            updated_dashboard.view_name
+        )
+        self.assertIsNotNone(view_menu_after_title_changed)
+        self.assertEqual(view_menu_after_title_changed.id, excepted_view_id)
+
+    def assert_permissions_were_deleted(self, deleted_dashboard):
+        view_menu = security_manager.find_view_menu(deleted_dashboard.view_name)
+        self.assertIsNone(view_menu)
+
+    def save_dash_basic_case(self, username=ADMIN_USERNAME):
+        # arrange
+        self.login(username=username)
+        (
+            dashboard_to_save,
+            data_before_change,
+            data_after_change,
+        ) = build_save_dash_parts()
+
+        # act
+        save_dash_response = self.save_dashboard_via_view(
+            dashboard_to_save.id, data_after_change
+        )
+
+        # assert
+        self.assertIn("SUCCESS", save_dash_response)
+
+        # post test
+        self.save_dashboard(dashboard_to_save.id, data_before_change)
+
+    def clean_created_objects(self):
+        with app.test_request_context():
+            self.logout()
+            self.login("admin")
+            delete_all_inserted_objects()
+            self.logout()
diff --git a/tests/dashboards/consts.py b/tests/dashboards/consts.py
new file mode 100644
index 0000000..a6e3683
--- /dev/null
+++ b/tests/dashboards/consts.py
@@ -0,0 +1,43 @@
+# 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.
+
+QUERY_FORMAT = "?q={}"
+
+DASHBOARDS_API_URL = "api/v1/dashboard/"
+DASHBOARDS_API_URL_WITH_QUERY_FORMAT = DASHBOARDS_API_URL + QUERY_FORMAT
+DASHBOARD_API_URL_FORMAT = DASHBOARDS_API_URL + "{}"
+EXPORT_DASHBOARDS_API_URL = DASHBOARDS_API_URL + "export/"
+EXPORT_DASHBOARDS_API_URL_WITH_QUERY_FORMAT = EXPORT_DASHBOARDS_API_URL + QUERY_FORMAT
+
+GET_DASHBOARD_VIEW_URL_FORMAT = "/superset/dashboard/{}/"
+SAVE_DASHBOARD_URL_FORMAT = "/superset/save_dash/{}/"
+COPY_DASHBOARD_URL_FORMAT = "/superset/copy_dash/{}/"
+ADD_SLICES_URL_FORMAT = "/superset/add_slices/{}/"
+
+DELETE_DASHBOARD_VIEW_URL_FORMAT = "/dashboard/delete/{}"
+GET_DASHBOARDS_LIST_VIEW = "/dashboard/list/"
+NEW_DASHBOARD_URL = "/dashboard/new/"
+GET_CHARTS_API_URL = "/api/v1/chart/"
+
+GAMMA_ROLE_NAME = "Gamma"
+
+ADMIN_USERNAME = "admin"
+GAMMA_USERNAME = "gamma"
+
+DASHBOARD_SLUG_OF_ACCESSIBLE_TABLE = "births"
+DEFAULT_DASHBOARD_SLUG_TO_TEST = "births"
+WORLD_HEALTH_SLUG = "world_health"
diff --git a/tests/dashboards/dashboard_test_utils.py b/tests/dashboards/dashboard_test_utils.py
new file mode 100644
index 0000000..c7032f8
--- /dev/null
+++ b/tests/dashboards/dashboard_test_utils.py
@@ -0,0 +1,121 @@
+# 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.
+import logging
+import random
+import string
+from typing import Any, Dict, List, Optional, Tuple
+
+from sqlalchemy import func
+
+from superset import appbuilder, db, security_manager
+from superset.connectors.sqla.models import SqlaTable
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from tests.dashboards.consts import DEFAULT_DASHBOARD_SLUG_TO_TEST
+
+logger = logging.getLogger(__name__)
+
+session = appbuilder.get_session
+
+
+def get_mock_positions(dashboard: Dashboard) -> Dict[str, Any]:
+    positions = {"DASHBOARD_VERSION_KEY": "v2"}
+    for i, slc in enumerate(dashboard.slices):
+        id_ = "DASHBOARD_CHART_TYPE-{}".format(i)
+        position_data: Any = {
+            "type": "CHART",
+            "id": id_,
+            "children": [],
+            "meta": {"width": 4, "height": 50, "chartId": slc.id},
+        }
+        positions[id_] = position_data
+    return positions
+
+
+def build_save_dash_parts(
+    dashboard_slug: Optional[str] = None, dashboard_to_edit: Optional[Dashboard] = None
+) -> Tuple[Dashboard, Dict[str, Any], Dict[str, Any]]:
+    if not dashboard_to_edit:
+        dashboard_slug = (
+            dashboard_slug if dashboard_slug else DEFAULT_DASHBOARD_SLUG_TO_TEST
+        )
+        dashboard_to_edit = get_dashboard_by_slug(dashboard_slug)
+
+    data_before_change = {
+        "positions": dashboard_to_edit.position,
+        "dashboard_title": dashboard_to_edit.dashboard_title,
+    }
+    data_after_change = {
+        "css": "",
+        "expanded_slices": {},
+        "positions": get_mock_positions(dashboard_to_edit),
+        "dashboard_title": dashboard_to_edit.dashboard_title,
+    }
+    return dashboard_to_edit, data_before_change, data_after_change
+
+
+def get_all_dashboards() -> List[Dashboard]:
+    return db.session.query(Dashboard).all()
+
+
+def get_dashboard_by_slug(dashboard_slug: str) -> Dashboard:
+    return db.session.query(Dashboard).filter_by(slug=dashboard_slug).first()
+
+
+def get_slice_by_name(slice_name: str) -> Slice:
+    return db.session.query(Slice).filter_by(slice_name=slice_name).first()
+
+
+def get_sql_table_by_name(table_name: str):
+    return db.session.query(SqlaTable).filter_by(table_name=table_name).one()
+
+
+def count_dashboards() -> int:
+    return db.session.query(func.count(Dashboard.id)).first()[0]
+
+
+def random_title():
+    return f"title{random_str()}"
+
+
+def random_slug():
+    return f"slug{random_str()}"
+
+
+def get_random_string(length):
+    letters = string.ascii_lowercase
+    result_str = "".join(random.choice(letters) for i in range(length))
+    print("Random string of length", length, "is:", result_str)
+    return result_str
+
+
+def random_str():
+    return get_random_string(8)
+
+
+def grant_access_to_dashboard(dashboard, role_name):
+    role = security_manager.find_role(role_name)
+    dashboard.roles.append(role)
+    db.session.merge(dashboard)
+    db.session.commit()
+
+
+def revoke_access_to_dashboard(dashboard, role_name):
+    role = security_manager.find_role(role_name)
+    dashboard.roles.remove(role)
+    db.session.merge(dashboard)
+    db.session.commit()
diff --git a/tests/dashboards/security/__init__.py b/tests/dashboards/security/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/dashboards/security/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/tests/dashboards/security/base_case.py b/tests/dashboards/security/base_case.py
new file mode 100644
index 0000000..ab24734
--- /dev/null
+++ b/tests/dashboards/security/base_case.py
@@ -0,0 +1,86 @@
+# 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 typing import List, Optional
+
+from flask import escape, Response
+
+from superset.models.dashboard import Dashboard
+from tests.dashboards.base_case import DashboardTestCase
+
+
+class BaseTestDashboardSecurity(DashboardTestCase):
+    def tearDown(self) -> None:
+        self.clean_created_objects()
+
+    def assert_dashboard_view_response(
+        self, response: Response, dashboard_to_access: Dashboard
+    ) -> None:
+        self.assert200(response)
+        assert escape(dashboard_to_access.dashboard_title) in response.data.decode(
+            "utf-8"
+        )
+
+    def assert_dashboard_api_response(
+        self, response: Response, dashboard_to_access: Dashboard
+    ) -> None:
+        self.assert200(response)
+        assert response.json["id"] == dashboard_to_access.id
+
+    def assert_dashboards_list_view_response(
+        self,
+        response: Response,
+        expected_counts: int,
+        expected_dashboards: Optional[List[Dashboard]] = None,
+        not_expected_dashboards: Optional[List[Dashboard]] = None,
+    ) -> None:
+        self.assert200(response)
+        response_html = response.data.decode("utf-8")
+        if expected_counts == 0:
+            assert "No records found" in response_html
+        else:
+            # # a way to parse number of dashboards returns
+            # in the list view as an html response
+            assert (
+                "Record Count:</strong> {count}".format(count=str(expected_counts))
+                in response_html
+            )
+        expected_dashboards = expected_dashboards or []
+        for dashboard in expected_dashboards:
+            assert dashboard.url in response_html
+        not_expected_dashboards = not_expected_dashboards or []
+        for dashboard in not_expected_dashboards:
+            assert dashboard.url not in response_html
+
+    def assert_dashboards_api_response(
+        self,
+        response: Response,
+        expected_counts: int,
+        expected_dashboards: Optional[List[Dashboard]] = None,
+        not_expected_dashboards: Optional[List[Dashboard]] = None,
+    ) -> None:
+        self.assert200(response)
+        response_data = response.json
+        assert response_data["count"] == expected_counts
+        response_dashboards_url = set(
+            map(lambda dash: dash["url"], response_data["result"])
+        )
+        expected_dashboards = expected_dashboards or []
+        for dashboard in expected_dashboards:
+            assert dashboard.url in response_dashboards_url
+        not_expected_dashboards = not_expected_dashboards or []
+        for dashboard in not_expected_dashboards:
+            assert dashboard.url not in response_dashboards_url
diff --git a/tests/dashboards/security/security_dataset_tests.py b/tests/dashboards/security/security_dataset_tests.py
new file mode 100644
index 0000000..842f5c0
--- /dev/null
+++ b/tests/dashboards/security/security_dataset_tests.py
@@ -0,0 +1,241 @@
+# 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.
+"""Unit tests for Superset"""
+import json
+
+import prison
+import pytest
+from flask import escape
+
+from superset import app
+from superset.models import core as models
+from tests.dashboards.base_case import DashboardTestCase
+from tests.dashboards.consts import *
+from tests.dashboards.dashboard_test_utils import *
+from tests.dashboards.superset_factory_util import *
+from tests.fixtures.energy_dashboard import load_energy_table_with_slice
+
+
+class TestDashboardDatasetSecurity(DashboardTestCase):
+    @pytest.fixture
+    def load_dashboard(self):
+        with app.app_context():
+            table = (
+                db.session.query(SqlaTable).filter_by(table_name="energy_usage").one()
+            )
+            # get a slice from the allowed table
+            slice = db.session.query(Slice).filter_by(slice_name="Energy Sankey").one()
+
+            self.grant_public_access_to_table(table)
+
+            pytest.hidden_dash_slug = f"hidden_dash_{random_slug()}"
+            pytest.published_dash_slug = f"published_dash_{random_slug()}"
+
+            # Create a published and hidden dashboard and add them to the database
+            published_dash = Dashboard()
+            published_dash.dashboard_title = "Published Dashboard"
+            published_dash.slug = pytest.published_dash_slug
+            published_dash.slices = [slice]
+            published_dash.published = True
+
+            hidden_dash = Dashboard()
+            hidden_dash.dashboard_title = "Hidden Dashboard"
+            hidden_dash.slug = pytest.hidden_dash_slug
+            hidden_dash.slices = [slice]
+            hidden_dash.published = False
+
+            db.session.merge(published_dash)
+            db.session.merge(hidden_dash)
+            yield db.session.commit()
+
+            self.revoke_public_access_to_table(table)
+            db.session.delete(published_dash)
+            db.session.delete(hidden_dash)
+            db.session.commit()
+
+    def test_dashboard_access__admin_can_access_all(self):
+        # arrange
+        self.login(username=ADMIN_USERNAME)
+        dashboard_title_by_url = {
+            dash.url: dash.dashboard_title for dash in get_all_dashboards()
+        }
+
+        # act
+        responses_by_url = {
+            url: self.client.get(url).data.decode("utf-8")
+            for url in dashboard_title_by_url.keys()
+        }
+
+        # assert
+        for dashboard_url, get_dashboard_response in responses_by_url.items():
+            assert (
+                escape(dashboard_title_by_url[dashboard_url]) in get_dashboard_response
+            )
+
+    def test_get_dashboards__users_are_dashboards_owners(self):
+        # arrange
+        username = "gamma"
+        user = security_manager.find_user(username)
+        my_owned_dashboard = create_dashboard_to_db(
+            dashboard_title="My Dashboard", published=False, owners=[user],
+        )
+
+        not_my_owned_dashboard = create_dashboard_to_db(
+            dashboard_title="Not My Dashboard", published=False,
+        )
+
+        self.login(user.username)
+
+        # act
+        get_dashboards_response = self.get_resp(DASHBOARDS_API_URL)
+
+        # assert
+        self.assertIn(my_owned_dashboard.url, get_dashboards_response)
+        self.assertNotIn(not_my_owned_dashboard.url, get_dashboards_response)
+
+    def test_get_dashboards__owners_can_view_empty_dashboard(self):
+        # arrange
+        dash = create_dashboard_to_db("Empty Dashboard", slug="empty_dashboard")
+        dashboard_url = dash.url
+        gamma_user = security_manager.find_user("gamma")
+        self.login(gamma_user.username)
+
+        # act
+        get_dashboards_response = self.get_resp(DASHBOARDS_API_URL)
+
+        # assert
+        self.assertNotIn(dashboard_url, get_dashboards_response)
+
+    def test_get_dashboards__users_can_view_favorites_dashboards(self):
+        # arrange
+        user = security_manager.find_user("gamma")
+        fav_dash_slug = f"my_favorite_dash_{random_slug()}"
+        regular_dash_slug = f"regular_dash_{random_slug()}"
+
+        favorite_dash = Dashboard()
+        favorite_dash.dashboard_title = "My Favorite Dashboard"
+        favorite_dash.slug = fav_dash_slug
+
+        regular_dash = Dashboard()
+        regular_dash.dashboard_title = "A Plain Ol Dashboard"
+        regular_dash.slug = regular_dash_slug
+
+        db.session.merge(favorite_dash)
+        db.session.merge(regular_dash)
+        db.session.commit()
+
+        dash = db.session.query(Dashboard).filter_by(slug=fav_dash_slug).first()
+
+        favorites = models.FavStar()
+        favorites.obj_id = dash.id
+        favorites.class_name = "Dashboard"
+        favorites.user_id = user.id
+
+        db.session.merge(favorites)
+        db.session.commit()
+
+        self.login(user.username)
+
+        # act
+        get_dashboards_response = self.get_resp(DASHBOARDS_API_URL)
+
+        # assert
+        self.assertIn(f"/superset/dashboard/{fav_dash_slug}/", get_dashboards_response)
+
+    def test_get_dashboards__user_can_not_view_unpublished_dash(self):
+        # arrange
+        admin_user = security_manager.find_user(ADMIN_USERNAME)
+        gamma_user = security_manager.find_user(GAMMA_USERNAME)
+        admin_and_not_published_dashboard = create_dashboard_to_db(
+            dashboard_title="admin_owned_unpublished_dash", owners=[admin_user]
+        )
+
+        self.login(gamma_user.username)
+
+        # act - list dashboards as a gamma user
+        get_dashboards_response_as_gamma = self.get_resp(DASHBOARDS_API_URL)
+
+        # assert
+        self.assertNotIn(
+            admin_and_not_published_dashboard.url, get_dashboards_response_as_gamma
+        )
+
+    @pytest.mark.usefixtures("load_energy_table_with_slice", "load_dashboard")
+    def test_get_dashboards__users_can_view_permitted_dashboard(self):
+        # arrange
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        self.create_user_with_roles(username, [new_role], should_create_roles=True)
+        accessed_table = get_sql_table_by_name("energy_usage")
+        self.grant_role_access_to_table(accessed_table, new_role)
+        # get a slice from the allowed table
+        slice_to_add_to_dashboards = get_slice_by_name("Energy Sankey")
+        # Create a published and hidden dashboard and add them to the database
+        first_dash = create_dashboard_to_db(
+            dashboard_title="Published Dashboard",
+            published=True,
+            slices=[slice_to_add_to_dashboards],
+        )
+
+        second_dash = create_dashboard_to_db(
+            dashboard_title="Hidden Dashboard",
+            published=True,
+            slices=[slice_to_add_to_dashboards],
+        )
+
+        try:
+            self.login(username)
+            # act
+            get_dashboards_response = self.get_resp(DASHBOARDS_API_URL)
+
+            # assert
+            self.assertIn(second_dash.url, get_dashboards_response)
+            self.assertIn(first_dash.url, get_dashboards_response)
+        finally:
+            self.revoke_public_access_to_table(accessed_table)
+
+    def test_get_dashboard_api_no_data_access(self):
+        """
+        Dashboard API: Test get dashboard without data access
+        """
+        admin = self.get_user("admin")
+        dashboard = create_dashboard_to_db(
+            random_title(), random_slug(), owners=[admin]
+        )
+
+        self.login(username="gamma")
+        uri = DASHBOARD_API_URL_FORMAT.format(dashboard.id)
+        rv = self.client.get(uri)
+        self.assert404(rv)
+
+    def test_get_dashboards_api_no_data_access(self):
+        """
+        Dashboard API: Test get dashboards no data access
+        """
+        admin = self.get_user("admin")
+        title = f"title{random_str()}"
+        create_dashboard_to_db(title, "slug1", owners=[admin])
+
+        self.login(username="gamma")
+        arguments = {
+            "filters": [{"col": "dashboard_title", "opr": "sw", "value": title[0:8]}]
+        }
+        uri = DASHBOARDS_API_URL_WITH_QUERY_FORMAT.format(prison.dumps(arguments))
+        rv = self.client.get(uri)
+        self.assert200(rv)
+        data = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(0, data["count"])
diff --git a/tests/dashboards/security/security_rbac_tests.py b/tests/dashboards/security/security_rbac_tests.py
new file mode 100644
index 0000000..efd2c99
--- /dev/null
+++ b/tests/dashboards/security/security_rbac_tests.py
@@ -0,0 +1,308 @@
+# 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.
+"""Unit tests for Superset"""
+from unittest import mock
+
+from tests.dashboards.dashboard_test_utils import *
+from tests.dashboards.security.base_case import BaseTestDashboardSecurity
+from tests.dashboards.superset_factory_util import (
+    create_dashboard_to_db,
+    create_database_to_db,
+    create_datasource_table_to_db,
+    create_slice_to_db,
+)
+
+
+@mock.patch.dict(
+    "superset.extensions.feature_flag_manager._feature_flags", DASHBOARD_RBAC=True,
+)
+class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
+    def test_get_dashboards_list__admin_get_all_dashboards(self):
+        # arrange
+        create_dashboard_to_db(
+            owners=[], slices=[create_slice_to_db()], published=False
+        )
+        dashboard_counts = count_dashboards()
+
+        self.login("admin")
+
+        # act
+        response = self.get_dashboards_list_response()
+
+        # assert
+        self.assert_dashboards_list_view_response(response, dashboard_counts)
+
+    def test_get_dashboards_list__owner_get_all_owned_dashboards(self):
+        # arrange
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        owner = self.create_user_with_roles(
+            username, [new_role], should_create_roles=True
+        )
+        database = create_database_to_db()
+        table = create_datasource_table_to_db(db_id=database.id, owners=[owner])
+        first_dash = create_dashboard_to_db(
+            owners=[owner], slices=[create_slice_to_db(datasource_id=table.id)]
+        )
+        second_dash = create_dashboard_to_db(
+            owners=[owner], slices=[create_slice_to_db(datasource_id=table.id)]
+        )
+        owned_dashboards = [first_dash, second_dash]
+        not_owned_dashboards = [
+            create_dashboard_to_db(
+                slices=[create_slice_to_db(datasource_id=table.id)], published=True
+            )
+        ]
+
+        self.login(username)
+
+        # act
+        response = self.get_dashboards_list_response()
+
+        # assert
+        self.assert_dashboards_list_view_response(
+            response, 2, owned_dashboards, not_owned_dashboards
+        )
+
+    def test_get_dashboards_list__user_without_any_permissions_get_empty_list(self):
+
+        # arrange
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        self.create_user_with_roles(username, [new_role], should_create_roles=True)
+
+        create_dashboard_to_db(published=True)
+        self.login(username)
+
+        # act
+        response = self.get_dashboards_list_response()
+
+        # assert
+        self.assert_dashboards_list_view_response(response, 0)
+
+    def test_get_dashboards_list__user_get_only_published_permitted_dashboards(self):
+        # arrange
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        self.create_user_with_roles(username, [new_role], should_create_roles=True)
+
+        published_dashboards = [
+            create_dashboard_to_db(published=True),
+            create_dashboard_to_db(published=True),
+        ]
+        not_published_dashboards = [
+            create_dashboard_to_db(published=False),
+            create_dashboard_to_db(published=False),
+        ]
+
+        for dash in published_dashboards + not_published_dashboards:
+            grant_access_to_dashboard(dash, new_role)
+
+        self.login(username)
+
+        # act
+        response = self.get_dashboards_list_response()
+
+        # assert
+        self.assert_dashboards_list_view_response(
+            response,
+            len(published_dashboards),
+            published_dashboards,
+            not_published_dashboards,
+        )
+
+        # post
+        for dash in published_dashboards + not_published_dashboards:
+            revoke_access_to_dashboard(dash, new_role)
+
+    def test_get_dashboards_list__public_user_without_any_permissions_get_empty_list(
+        self,
+    ):
+        create_dashboard_to_db(published=True)
+
+        # act
+        response = self.get_dashboards_list_response()
+
+        # assert
+        self.assert_dashboards_list_view_response(response, 0)
+
+    def test_get_dashboards_list__public_user_get_only_published_permitted_dashboards(
+        self,
+    ):
+        # arrange
+        published_dashboards = [
+            create_dashboard_to_db(published=True),
+            create_dashboard_to_db(published=True),
+        ]
+        not_published_dashboards = [
+            create_dashboard_to_db(published=False),
+            create_dashboard_to_db(published=False),
+        ]
+
+        for dash in published_dashboards + not_published_dashboards:
+            grant_access_to_dashboard(dash, "Public")
+
+        # act
+        response = self.get_dashboards_list_response()
+
+        # assert
+        self.assert_dashboards_list_view_response(
+            response,
+            len(published_dashboards),
+            published_dashboards,
+            not_published_dashboards,
+        )
+
+        # post
+        for dash in published_dashboards + not_published_dashboards:
+            revoke_access_to_dashboard(dash, "Public")
+
+    def test_get_dashboards_api__admin_get_all_dashboards(self):
+        # arrange
+        create_dashboard_to_db(
+            owners=[], slices=[create_slice_to_db()], published=False
+        )
+        dashboard_counts = count_dashboards()
+
+        self.login("admin")
+
+        # act
+        response = self.get_dashboards_api_response()
+
+        # assert
+        self.assert_dashboards_api_response(response, dashboard_counts)
+
+    def test_get_dashboards_api__owner_get_all_owned_dashboards(self):
+        # arrange
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        owner = self.create_user_with_roles(
+            username, [new_role], should_create_roles=True
+        )
+        database = create_database_to_db()
+        table = create_datasource_table_to_db(db_id=database.id, owners=[owner])
+        first_dash = create_dashboard_to_db(
+            owners=[owner], slices=[create_slice_to_db(datasource_id=table.id)]
+        )
+        second_dash = create_dashboard_to_db(
+            owners=[owner], slices=[create_slice_to_db(datasource_id=table.id)]
+        )
+        owned_dashboards = [first_dash, second_dash]
+        not_owned_dashboards = [
+            create_dashboard_to_db(
+                slices=[create_slice_to_db(datasource_id=table.id)], published=True
+            )
+        ]
+
+        self.login(username)
+
+        # act
+        response = self.get_dashboards_api_response()
+
+        # assert
+        self.assert_dashboards_api_response(
+            response, 2, owned_dashboards, not_owned_dashboards
+        )
+
+    def test_get_dashboards_api__user_without_any_permissions_get_empty_list(self):
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        self.create_user_with_roles(username, [new_role], should_create_roles=True)
+        create_dashboard_to_db(published=True)
+        self.login(username)
+
+        # act
+        response = self.get_dashboards_api_response()
+
+        # assert
+        self.assert_dashboards_api_response(response, 0)
+
+    def test_get_dashboards_api__user_get_only_published_permitted_dashboards(self):
+        username = random_str()
+        new_role = f"role_{random_str()}"
+        self.create_user_with_roles(username, [new_role], should_create_roles=True)
+        # arrange
+        published_dashboards = [
+            create_dashboard_to_db(published=True),
+            create_dashboard_to_db(published=True),
+        ]
+        not_published_dashboards = [
+            create_dashboard_to_db(published=False),
+            create_dashboard_to_db(published=False),
+        ]
+
+        for dash in published_dashboards + not_published_dashboards:
+            grant_access_to_dashboard(dash, new_role)
+
+        self.login(username)
+
+        # act
+        response = self.get_dashboards_api_response()
+
+        # assert
+        self.assert_dashboards_api_response(
+            response,
+            len(published_dashboards),
+            published_dashboards,
+            not_published_dashboards,
+        )
+
+        # post
+        for dash in published_dashboards + not_published_dashboards:
+            revoke_access_to_dashboard(dash, new_role)
+
+    def test_get_dashboards_api__public_user_without_any_permissions_get_empty_list(
+        self,
+    ):
+        create_dashboard_to_db(published=True)
+
+        # act
+        response = self.get_dashboards_api_response()
+
+        # assert
+        self.assert_dashboards_api_response(response, 0)
+
+    def test_get_dashboards_api__public_user_get_only_published_permitted_dashboards(
+        self,
+    ):
+        # arrange
+        published_dashboards = [
+            create_dashboard_to_db(published=True),
+            create_dashboard_to_db(published=True),
+        ]
+        not_published_dashboards = [
+            create_dashboard_to_db(published=False),
+            create_dashboard_to_db(published=False),
+        ]
+
+        for dash in published_dashboards + not_published_dashboards:
+            grant_access_to_dashboard(dash, "Public")
+
+        # act
+        response = self.get_dashboards_api_response()
+
+        # assert
+        self.assert_dashboards_api_response(
+            response,
+            len(published_dashboards),
+            published_dashboards,
+            not_published_dashboards,
+        )
+
+        # post
+        for dash in published_dashboards + not_published_dashboards:
+            revoke_access_to_dashboard(dash, "Public")
diff --git a/tests/dashboards/superset_factory_util.py b/tests/dashboards/superset_factory_util.py
new file mode 100644
index 0000000..f62f4ac
--- /dev/null
+++ b/tests/dashboards/superset_factory_util.py
@@ -0,0 +1,305 @@
+# 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.
+import logging
+from typing import List, Optional
+
+from flask_appbuilder import Model
+from flask_appbuilder.security.sqla.models import User
+
+from superset import appbuilder
+from superset.connectors.sqla.models import SqlaTable, sqlatable_user
+from superset.models.core import Database
+from superset.models.dashboard import (
+    Dashboard,
+    dashboard_slices,
+    dashboard_user,
+    DashboardRoles,
+)
+from superset.models.slice import Slice, slice_user
+from tests.dashboards.dashboard_test_utils import random_slug, random_str, random_title
+
+logger = logging.getLogger(__name__)
+
+session = appbuilder.get_session
+
+inserted_dashboards_ids = []
+inserted_databases_ids = []
+inserted_sqltables_ids = []
+inserted_slices_ids = []
+
+
+def create_dashboard_to_db(
+    dashboard_title: Optional[str] = None,
+    slug: Optional[str] = None,
+    published: bool = False,
+    owners: Optional[List[User]] = None,
+    slices: Optional[List[Slice]] = None,
+    css: str = "",
+    json_metadata: str = "",
+    position_json: str = "",
+) -> Dashboard:
+    dashboard = create_dashboard(
+        dashboard_title,
+        slug,
+        published,
+        owners,
+        slices,
+        css,
+        json_metadata,
+        position_json,
+    )
+
+    insert_model(dashboard)
+    inserted_dashboards_ids.append(dashboard.id)
+    return dashboard
+
+
+def create_dashboard(
+    dashboard_title: Optional[str] = None,
+    slug: Optional[str] = None,
+    published: bool = False,
+    owners: Optional[List[User]] = None,
+    slices: Optional[List[Slice]] = None,
+    css: str = "",
+    json_metadata: str = "",
+    position_json: str = "",
+) -> Dashboard:
+    dashboard_title = dashboard_title or random_title()
+    slug = slug or random_slug()
+    owners = owners or []
+    slices = slices or []
+    return Dashboard(
+        dashboard_title=dashboard_title,
+        slug=slug,
+        published=published,
+        owners=owners,
+        css=css,
+        position_json=position_json,
+        json_metadata=json_metadata,
+        slices=slices,
+    )
+
+
+def insert_model(dashboard: Model) -> None:
+    session.add(dashboard)
+    session.commit()
+    session.refresh(dashboard)
+
+
+def create_slice_to_db(
+    name: Optional[str] = None,
+    datasource_id: Optional[int] = None,
+    owners: Optional[List[User]] = None,
+) -> Slice:
+    slice_ = create_slice(datasource_id, name, owners)
+    insert_model(slice_)
+    inserted_slices_ids.append(slice_.id)
+    return slice_
+
+
+def create_slice(
+    datasource_id: Optional[int], name: Optional[str], owners: Optional[List[User]]
+) -> Slice:
+    name = name or random_str()
+    owners = owners or []
+    datasource_id = (
+        datasource_id or create_datasource_table_to_db(name=name + "_table").id
+    )
+    return Slice(
+        slice_name=name,
+        datasource_id=datasource_id,
+        owners=owners,
+        datasource_type="table",
+    )
+
+
+def create_datasource_table_to_db(
+    name: Optional[str] = None,
+    db_id: Optional[int] = None,
+    owners: Optional[List[User]] = None,
+) -> SqlaTable:
+    sqltable = create_datasource_table(name, db_id, owners)
+    insert_model(sqltable)
+    inserted_sqltables_ids.append(sqltable.id)
+    return sqltable
+
+
+def create_datasource_table(
+    name: Optional[str] = None,
+    db_id: Optional[int] = None,
+    owners: Optional[List[User]] = None,
+) -> SqlaTable:
+    name = name or random_str()
+    owners = owners or []
+    db_id = db_id or create_database_to_db(name=name + "_db").id
+    return SqlaTable(table_name=name, database_id=db_id, owners=owners)
+
+
+def create_database_to_db(name: Optional[str] = None) -> Database:
+    database = create_database(name)
+    insert_model(database)
+    inserted_databases_ids.append(database.id)
+    return database
+
+
+def create_database(name: Optional[str] = None) -> Database:
+    name = name or random_str()
+    return Database(database_name=name, sqlalchemy_uri="sqlite:///:memory:")
+
+
+def delete_all_inserted_objects() -> None:
+    delete_all_inserted_dashboards()
+    delete_all_inserted_slices()
+    delete_all_inserted_tables()
+    delete_all_inserted_dbs()
+
+
+def delete_all_inserted_dashboards():
+    try:
+        dashboards_to_delete: List[Dashboard] = session.query(Dashboard).filter(
+            Dashboard.id.in_(inserted_dashboards_ids)
+        ).all()
+        for dashboard in dashboards_to_delete:
+            try:
+                delete_dashboard(dashboard, False)
+            except Exception as ex:
+                logger.error(f"failed to delete {dashboard.id}", exc_info=True)
+                raise ex
+        if len(inserted_dashboards_ids) > 0:
+            session.commit()
+            inserted_dashboards_ids.clear()
+    except Exception as ex2:
+        logger.error("delete_all_inserted_dashboards failed", exc_info=True)
+        raise ex2
+
+
+def delete_dashboard(dashboard: Dashboard, do_commit: bool = False) -> None:
+    logger.info(f"deleting dashboard{dashboard.id}")
+    delete_dashboard_roles_associations(dashboard)
+    delete_dashboard_users_associations(dashboard)
+    delete_dashboard_slices_associations(dashboard)
+    session.delete(dashboard)
+    if do_commit:
+        session.commit()
+
+
+def delete_dashboard_users_associations(dashboard: Dashboard) -> None:
+    session.execute(
+        dashboard_user.delete().where(dashboard_user.c.dashboard_id == dashboard.id)
+    )
+
+
+def delete_dashboard_roles_associations(dashboard: Dashboard) -> None:
+    session.execute(
+        DashboardRoles.delete().where(DashboardRoles.c.dashboard_id == dashboard.id)
+    )
+
+
+def delete_dashboard_slices_associations(dashboard: Dashboard) -> None:
+    session.execute(
+        dashboard_slices.delete().where(dashboard_slices.c.dashboard_id == dashboard.id)
+    )
+
+
+def delete_all_inserted_slices():
+    try:
+        slices_to_delete: List[Slice] = session.query(Slice).filter(
+            Slice.id.in_(inserted_slices_ids)
+        ).all()
+        for slice in slices_to_delete:
+            try:
+                delete_slice(slice, False)
+            except Exception as ex:
+                logger.error(f"failed to delete {slice.id}", exc_info=True)
+                raise ex
+        if len(inserted_slices_ids) > 0:
+            session.commit()
+            inserted_slices_ids.clear()
+    except Exception as ex2:
+        logger.error("delete_all_inserted_slices failed", exc_info=True)
+        raise ex2
+
+
+def delete_slice(slice_: Slice, do_commit: bool = False) -> None:
+    logger.info(f"deleting slice{slice_.id}")
+    delete_slice_users_associations(slice_)
+    session.delete(slice_)
+    if do_commit:
+        session.commit()
+
+
+def delete_slice_users_associations(slice_: Slice) -> None:
+    session.execute(slice_user.delete().where(slice_user.c.slice_id == slice_.id))
+
+
+def delete_all_inserted_tables():
+    try:
+        tables_to_delete: List[SqlaTable] = session.query(SqlaTable).filter(
+            SqlaTable.id.in_(inserted_sqltables_ids)
+        ).all()
+        for table in tables_to_delete:
+            try:
+                delete_sqltable(table, False)
+            except Exception as ex:
+                logger.error(f"failed to delete {table.id}", exc_info=True)
+                raise ex
+        if len(inserted_sqltables_ids) > 0:
+            session.commit()
+            inserted_sqltables_ids.clear()
+    except Exception as ex2:
+        logger.error("delete_all_inserted_tables failed", exc_info=True)
+        raise ex2
+
+
+def delete_sqltable(table: SqlaTable, do_commit: bool = False) -> None:
+    logger.info(f"deleting table{table.id}")
+    delete_table_users_associations(table)
+    session.delete(table)
+    if do_commit:
+        session.commit()
+
+
+def delete_table_users_associations(table: SqlaTable) -> None:
+    session.execute(
+        sqlatable_user.delete().where(sqlatable_user.c.table_id == table.id)
+    )
+
+
+def delete_all_inserted_dbs():
+    try:
+        dbs_to_delete: List[Database] = session.query(Database).filter(
+            Database.id.in_(inserted_databases_ids)
+        ).all()
+        for db in dbs_to_delete:
+            try:
+                delete_database(db, False)
+            except Exception as ex:
+                logger.error(f"failed to delete {db.id}", exc_info=True)
+                raise ex
+        if len(inserted_databases_ids) > 0:
+            session.commit()
+            inserted_databases_ids.clear()
+    except Exception as ex2:
+        logger.error("delete_all_inserted_databases failed", exc_info=True)
+        raise ex2
+
+
+def delete_database(database: Database, do_commit: bool = False) -> None:
+    logger.info(f"deleting database{database.id}")
+    session.delete(database)
+    if do_commit:
+        session.commit()