Skip to content

Commit d96ba10

Browse files
villebroeschutho
authored andcommitted
fix(dashboard-rbac): use normal rbac when no roles chosen (#23586)
(cherry picked from commit a823033)
1 parent d04c2a5 commit d96ba10

File tree

8 files changed

+107
-44
lines changed

8 files changed

+107
-44
lines changed

docs/docs/creating-charts-dashboards/creating-your-first-dashboard.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,8 @@ Non-owner users access can be managed two different ways:
184184

185185
1. Dataset permissions - if you add to the relevant role permissions to datasets it automatically grants implicit access to all dashboards that uses those permitted datasets
186186
2. Dashboard roles - if you enable **DASHBOARD_RBAC** [feature flag](https://superset.apache.org/docs/installation/configuring-superset#feature-flags) then you be able to manage which roles can access the dashboard
187-
- Having dashboard access implicitly grants read access to the associated datasets, therefore
188-
all charts will load their data even if feature flag is turned on and no roles assigned
189-
to roles the access will fallback to **Dataset permissions**
187+
- Granting a role access to a dashboard will bypass dataset level checks. Having dashboard access implicitly grants read access to all the featured charts in the dashboard, and thereby also all the associated datasets.
188+
- If no roles are specified for a dashboard, regular **Dataset permissions** will apply.
190189

191190
<img src={useBaseUrl("/img/tutorial/tutorial_dashboard_access.png" )} />
192191

superset-frontend/src/dashboard/components/PropertiesModal/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ const PropertiesModal = ({
486486
</StyledFormItem>
487487
<p className="help-block">
488488
{t(
489-
'Roles is a list which defines access to the dashboard. Granting a role access to a dashboard will bypass dataset level checks. If no roles are defined, then the dashboard is available to all roles.',
489+
'Roles is a list which defines access to the dashboard. Granting a role access to a dashboard will bypass dataset level checks. If no roles are defined, regular access permissions apply.',
490490
)}
491491
</p>
492492
</Col>

superset/security/manager.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,7 +2044,10 @@ def raise_for_dashboard_access(self, dashboard: "Dashboard") -> None:
20442044
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
20452045

20462046
def has_rbac_access() -> bool:
2047-
return (not is_feature_enabled("DASHBOARD_RBAC")) or any(
2047+
if not is_feature_enabled("DASHBOARD_RBAC") or not dashboard.roles:
2048+
return True
2049+
2050+
return any(
20482051
dashboard_role.id
20492052
in [user_role.id for user_role in self.get_user_roles()]
20502053
for dashboard_role in dashboard.roles
@@ -2071,15 +2074,18 @@ def can_access_based_on_dashboard(datasource: "BaseDatasource") -> bool:
20712074
from superset.models.dashboard import Dashboard
20722075
from superset.models.slice import Slice
20732076

2077+
dashboard_ids = db.session.query(Dashboard.id)
2078+
dashboard_ids = DashboardAccessFilter(
2079+
"id", SQLAInterface(Dashboard, db.session)
2080+
).apply(dashboard_ids, None)
2081+
20742082
datasource_class = type(datasource)
20752083
query = (
2076-
db.session.query(datasource_class)
2084+
db.session.query(Dashboard.id)
2085+
.join(Slice, Dashboard.slices)
20772086
.join(Slice.table)
20782087
.filter(datasource_class.id == datasource.id)
2079-
)
2080-
2081-
query = DashboardAccessFilter("id", SQLAInterface(Dashboard, db.session)).apply(
2082-
query, None
2088+
.filter(Dashboard.id.in_(dashboard_ids))
20832089
)
20842090

20852091
exists = db.session.query(query.exists()).scalar()

superset/utils/decorators.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ def on_security_exception(self: Any, ex: Exception) -> Response:
116116

117117

118118
# noinspection PyPackageRequirements
119-
def check_dashboard_access(
120-
on_error: Callable[..., Any] = on_security_exception
121-
) -> Callable[..., Any]:
119+
def check_dashboard_access(on_error: Callable[[str], Any]) -> Callable[..., Any]:
122120
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
123121
@wraps(f)
124122
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
@@ -130,7 +128,7 @@ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
130128
try:
131129
current_app.appbuilder.sm.raise_for_dashboard_access(dashboard)
132130
except DashboardAccessDeniedError as ex:
133-
return on_error(self, ex)
131+
return on_error(str(ex))
134132
except Exception as exception:
135133
raise exception
136134

superset/views/core.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
TableColumn,
6868
)
6969
from superset.constants import QUERY_EARLY_CANCEL_KEY
70+
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
7071
from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand
7172
from superset.dashboards.dao import DashboardDAO
7273
from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand
@@ -167,6 +168,7 @@
167168
get_form_data,
168169
get_viz,
169170
loads_request_json,
171+
redirect_with_flash,
170172
sanitize_datasource_data,
171173
)
172174
from superset.viz import BaseViz
@@ -192,6 +194,7 @@
192194
"disable_data_preview",
193195
]
194196

197+
DASHBOARD_LIST_URL = "/dashboard/list/"
195198
DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted")
196199
USER_MISSING_ERR = __("The user seems to have been deleted")
197200
PARAMETER_MISSING_ERR = __(
@@ -1823,9 +1826,7 @@ def favstar( # pylint: disable=no-self-use
18231826
@expose("/dashboard/<dashboard_id_or_slug>/")
18241827
@event_logger.log_this_with_extra_payload
18251828
@check_dashboard_access(
1826-
on_error=lambda self, ex: Response(
1827-
utils.error_msg_from_exception(ex), status=403
1828-
)
1829+
on_error=lambda msg: redirect_with_flash(DASHBOARD_LIST_URL, msg, "danger")
18291830
)
18301831
def dashboard(
18311832
self,
@@ -1843,25 +1844,33 @@ def dashboard(
18431844
if not dashboard:
18441845
abort(404)
18451846

1846-
if config["ENABLE_ACCESS_REQUEST"]:
1847-
for datasource in dashboard.datasources:
1848-
datasource = DatasourceDAO.get_datasource(
1849-
datasource_type=DatasourceType(datasource.type),
1850-
datasource_id=datasource.id,
1851-
session=db.session(),
1847+
has_access_ = False
1848+
for datasource in dashboard.datasources:
1849+
datasource = DatasourceDAO.get_datasource(
1850+
datasource_type=DatasourceType(datasource.type),
1851+
datasource_id=datasource.id,
1852+
session=db.session(),
1853+
)
1854+
if datasource and security_manager.can_access_datasource(
1855+
datasource=datasource,
1856+
):
1857+
has_access_ = True
1858+
1859+
if has_access_ is False and config["ENABLE_ACCESS_REQUEST"]:
1860+
flash(
1861+
__(security_manager.get_datasource_access_error_msg(datasource)),
1862+
"danger",
18521863
)
1853-
if datasource and not security_manager.can_access_datasource(
1854-
datasource=datasource
1855-
):
1856-
flash(
1857-
__(
1858-
security_manager.get_datasource_access_error_msg(datasource)
1859-
),
1860-
"danger",
1861-
)
1862-
return redirect(
1863-
f"/superset/request_access/?dashboard_id={dashboard.id}"
1864-
)
1864+
return redirect(
1865+
f"/superset/request_access/?dashboard_id={dashboard.id}"
1866+
)
1867+
1868+
if has_access_:
1869+
break
1870+
1871+
if dashboard.datasources and not has_access_:
1872+
flash(DashboardAccessDeniedError.message, "danger")
1873+
return redirect(DASHBOARD_LIST_URL)
18651874

18661875
dash_edit_perm = security_manager.is_owner(
18671876
dashboard

superset/views/dashboard/mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class DashboardMixin: # pylint: disable=too-few-public-methods
6666
"roles": _(
6767
"Roles is a list which defines access to the dashboard. "
6868
"Granting a role access to a dashboard will bypass dataset level checks."
69-
"If no roles are defined then the dashboard is available to all roles."
69+
"If no roles are defined, regular access permissions apply."
7070
),
7171
"published": _(
7272
"Determines whether or not this dashboard is "

superset/views/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323
import msgpack
2424
import pyarrow as pa
2525
import simplejson as json
26-
from flask import g, has_request_context, request
26+
from flask import flash, g, has_request_context, redirect, request
2727
from flask_appbuilder.security.sqla import models as ab_models
2828
from flask_appbuilder.security.sqla.models import User
2929
from flask_babel import _
3030
from sqlalchemy.orm.exc import NoResultFound
31+
from werkzeug.wrappers.response import Response
3132

3233
import superset.models.core as models
3334
from superset import app, dataframe, db, result_set, viz
@@ -592,3 +593,8 @@ def get_cta_schema_name(
592593
if not func:
593594
return None
594595
return func(database, user, schema, sql)
596+
597+
598+
def redirect_with_flash(url: str, message: str, category: str) -> Response:
599+
flash(message=message, category=category)
600+
return redirect(url)

tests/integration_tests/dashboards/security/security_rbac_tests.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,11 @@ def test_get_dashboard_view__user_can_not_access_without_permission(self):
9191

9292
# act
9393
response = self.get_dashboard_view_response(dashboard_to_access)
94+
assert response.status_code == 302
9495

9596
request_payload = get_query_context("birth_names")
9697
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
97-
self.assertEqual(rv.status_code, 403)
98-
99-
# assert
100-
self.assert403(response)
98+
assert rv.status_code == 403
10199

102100
def test_get_dashboard_view__user_with_dashboard_permission_can_not_access_draft(
103101
self,
@@ -114,11 +112,57 @@ def test_get_dashboard_view__user_with_dashboard_permission_can_not_access_draft
114112
response = self.get_dashboard_view_response(dashboard_to_access)
115113

116114
# assert
117-
self.assert403(response)
115+
assert response.status_code == 302
118116

119117
# post
120118
revoke_access_to_dashboard(dashboard_to_access, new_role)
121119

120+
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
121+
def test_get_dashboard_view__user_no_access_regular_rbac(self):
122+
if backend() == "hive":
123+
return
124+
125+
slice = (
126+
db.session.query(Slice)
127+
.filter_by(slice_name="Girl Name Cloud")
128+
.one_or_none()
129+
)
130+
dashboard = create_dashboard_to_db(published=True, slices=[slice])
131+
self.login("gamma")
132+
133+
# assert redirect on regular rbac access denied
134+
response = self.get_dashboard_view_response(dashboard)
135+
assert response.status_code == 302
136+
137+
request_payload = get_query_context("birth_names")
138+
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
139+
assert rv.status_code == 403
140+
db.session.delete(dashboard)
141+
db.session.commit()
142+
143+
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
144+
def test_get_dashboard_view__user_access_regular_rbac(self):
145+
if backend() == "hive":
146+
return
147+
148+
slice = (
149+
db.session.query(Slice)
150+
.filter_by(slice_name="Girl Name Cloud")
151+
.one_or_none()
152+
)
153+
dashboard = create_dashboard_to_db(published=True, slices=[slice])
154+
self.login("gamma_sqllab")
155+
156+
response = self.get_dashboard_view_response(dashboard)
157+
158+
assert response.status_code == 200
159+
160+
request_payload = get_query_context("birth_names")
161+
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
162+
assert rv.status_code == 200
163+
db.session.delete(dashboard)
164+
db.session.commit()
165+
122166
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
123167
def test_get_dashboard_view__user_access_with_dashboard_permission(self):
124168
if backend() == "hive":
@@ -155,13 +199,14 @@ def test_get_dashboard_view__user_access_with_dashboard_permission(self):
155199
@pytest.mark.usefixtures("public_role_like_gamma")
156200
def test_get_dashboard_view__public_user_can_not_access_without_permission(self):
157201
dashboard_to_access = create_dashboard_to_db(published=True)
202+
grant_access_to_dashboard(dashboard_to_access, "Alpha")
158203
self.logout()
159204

160205
# act
161206
response = self.get_dashboard_view_response(dashboard_to_access)
162207

163208
# assert
164-
self.assert403(response)
209+
assert response.status_code == 302
165210

166211
@pytest.mark.usefixtures("public_role_like_gamma")
167212
def test_get_dashboard_view__public_user_with_dashboard_permission_can_not_access_draft(
@@ -175,7 +220,7 @@ def test_get_dashboard_view__public_user_with_dashboard_permission_can_not_acces
175220
response = self.get_dashboard_view_response(dashboard_to_access)
176221

177222
# assert
178-
self.assert403(response)
223+
assert response.status_code == 302
179224

180225
# post
181226
revoke_access_to_dashboard(dashboard_to_access, "Public")

0 commit comments

Comments
 (0)