Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard_rbac): provide data access based on dashboard access #13992

Merged
merged 18 commits into from
Apr 13, 2021
35 changes: 32 additions & 3 deletions superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from flask import current_app, g
from flask_appbuilder import Model
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.sqla.manager import SecurityManager
from flask_appbuilder.security.sqla.models import (
assoc_permissionview_role,
Expand Down Expand Up @@ -61,7 +62,6 @@
from superset.sql_parse import Table
from superset.viz import BaseViz


amitmiran137 marked this conversation as resolved.
Show resolved Hide resolved
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -923,7 +923,9 @@ def set_perm( # pylint: disable=no-self-use,unused-argument
)
)

def raise_for_access( # pylint: disable=too-many-arguments,too-many-branches
def raise_for_access(
# pylint: disable=too-many-arguments,too-many-branches,
# pylint: disable=too-many-locals
self,
database: Optional["Database"] = None,
datasource: Optional["BaseDatasource"] = None,
Expand Down Expand Up @@ -994,8 +996,14 @@ def raise_for_access( # pylint: disable=too-many-arguments,too-many-branches

assert datasource

from superset.extensions import feature_flag_manager

if not (
self.can_access_schema(datasource)
(
feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC")
and self.can_access_based_on_dashboard(datasource)
)
or self.can_access_schema(datasource)
amitmiran137 marked this conversation as resolved.
Show resolved Hide resolved
or self.can_access("datasource_access", datasource.perm or "")
):
raise SupersetSecurityException(
Expand Down Expand Up @@ -1125,3 +1133,24 @@ def raise_for_dashboard_access(self, dashboard: "Dashboard") -> None:

if not can_access:
raise DashboardAccessDeniedError()

# pylint: disable=no-self-use
def can_access_based_on_dashboard(self, datasource: "BaseDatasource") -> bool:
amitmiran137 marked this conversation as resolved.
Show resolved Hide resolved
from superset import db
from superset.dashboards.filters import DashboardFilter
from superset.models.slice import Slice
from superset.models.dashboard import Dashboard

table_type = ConnectorRegistry.sources["table"]
query = (
db.session.query(table_type)
.join(Slice.table)
.filter(table_type.id == datasource.id)
)

query = DashboardFilter("id", SQLAInterface(Dashboard, db.session)).apply(
query, None
)

exists = db.session.query(query.exists()).scalar()
return exists
28 changes: 25 additions & 3 deletions tests/dashboards/security/security_rbac_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
create_datasource_table_to_db,
create_slice_to_db,
)
from tests.fixtures.birth_names_dashboard import load_birth_names_dashboard_with_slices
from tests.fixtures.public_role import public_role_like_gamma
from tests.fixtures.query_context import get_query_context

CHART_DATA_URI = "api/v1/chart/data"


@mock.patch.dict(
Expand Down Expand Up @@ -65,16 +69,26 @@ def test_get_dashboard_view__owner_can_access(self):
# assert
self.assert_dashboard_view_response(response, dashboard_to_access)

@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_get_dashboard_view__user_can_not_access_without_permission(self):
username = random_str()
new_role = f"role_{random_str()}"
self.create_user_with_roles(username, [new_role], should_create_roles=True)
dashboard_to_access = create_dashboard_to_db(published=True)
slice = (
db.session.query(Slice)
.filter_by(slice_name="Girl Name Cloud")
.one_or_none()
)
dashboard_to_access = create_dashboard_to_db(published=True, slices=[slice])
self.login(username)

# act
response = self.get_dashboard_view_response(dashboard_to_access)

request_payload = get_query_context("birth_names")
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
self.assertEqual(rv.status_code, 401)

# assert
self.assert403(response)

Expand All @@ -98,16 +112,20 @@ def test_get_dashboard_view__user_with_dashboard_permission_can_not_access_draft
# post
revoke_access_to_dashboard(dashboard_to_access, new_role)

@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_get_dashboard_view__user_access_with_dashboard_permission(self):
# arrange

username = random_str()
new_role = f"role_{random_str()}"
self.create_user_with_roles(username, [new_role], should_create_roles=True)

dashboard_to_access = create_dashboard_to_db(
published=True, slices=[create_slice_to_db()]
slice = (
db.session.query(Slice)
.filter_by(slice_name="Girl Name Cloud")
.one_or_none()
)
dashboard_to_access = create_dashboard_to_db(published=True, slices=[slice])
self.login(username)
grant_access_to_dashboard(dashboard_to_access, new_role)

Expand All @@ -117,6 +135,10 @@ def test_get_dashboard_view__user_access_with_dashboard_permission(self):
# assert
self.assert_dashboard_view_response(response, dashboard_to_access)

request_payload = get_query_context("birth_names")
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
self.assertEqual(rv.status_code, 200)

# post
revoke_access_to_dashboard(dashboard_to_access, new_role)

Expand Down
1 change: 1 addition & 0 deletions tests/utils_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import os
import re
from typing import Any, Tuple, List, Optional
from unittest import mock
amitmiran137 marked this conversation as resolved.
Show resolved Hide resolved
from unittest.mock import Mock, patch
from tests.fixtures.birth_names_dashboard import load_birth_names_dashboard_with_slices

Expand Down