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

fix: embedded dashboard check #24690

Merged
merged 1 commit into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2006,28 +2006,37 @@
from superset import is_feature_enabled
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError

if self.is_guest_user() and dashboard.embedded:
if self.is_guest_user():

Check warning on line 2009 in superset/security/manager.py

View check run for this annotation

Codecov / codecov/patch

superset/security/manager.py#L2009

Added line #L2009 was not covered by tests
# Guest user is currently used for embedded dashboards only. If the guest user
# doesn't have access to the dashboard, ignore all other checks.
if self.has_guest_access(dashboard):
return
else:
if self.is_admin() or self.is_owner(dashboard):
return
raise DashboardAccessDeniedError()

Check warning on line 2014 in superset/security/manager.py

View check run for this annotation

Codecov / codecov/patch

superset/security/manager.py#L2014

Added line #L2014 was not covered by tests

# RBAC and legacy (datasource inferred) access controls.
if is_feature_enabled("DASHBOARD_RBAC") and dashboard.roles:
if dashboard.published and {role.id for role in dashboard.roles} & {
role.id for role in self.get_user_roles()
}:
return
elif (
not dashboard.published
or not dashboard.datasources
or any(
self.can_access_datasource(datasource)
for datasource in dashboard.datasources
)
):
if self.is_admin() or self.is_owner(dashboard):
return

Check warning on line 2017 in superset/security/manager.py

View check run for this annotation

Codecov / codecov/patch

superset/security/manager.py#L2016-L2017

Added lines #L2016 - L2017 were not covered by tests

# RBAC and legacy (datasource inferred) access controls.
if is_feature_enabled("DASHBOARD_RBAC") and dashboard.roles:
if dashboard.published and {role.id for role in dashboard.roles} & {

Check warning on line 2021 in superset/security/manager.py

View check run for this annotation

Codecov / codecov/patch

superset/security/manager.py#L2020-L2021

Added lines #L2020 - L2021 were not covered by tests
role.id for role in self.get_user_roles()
}:
return
elif (

Check warning on line 2025 in superset/security/manager.py

View check run for this annotation

Codecov / codecov/patch

superset/security/manager.py#L2025

Added line #L2025 was not covered by tests
# To understand why we rely on status and give access to draft dashboards
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the references. I agree that this logic is hard to understand and only exists for backward compatibility. I added a card to our major versions board to revisit this in 4.0 and break compatibility in favor of a clear and more restrictive logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second @michael-s-molina's comment. I think the security logic for dashboard access is rather convoluted and difficult to grok which is definitely undesirable from a security perspective, i.e., there likely are holes which can be exploited.

I wonder if in 4.0 if dashboard RBAC should no longer be a feature but rather the default which will likely simplify things even if (per @michael-s-molina's comment) this breaks compatibility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if in 4.0 if dashboard RBAC should no longer be a feature but rather the default which will likely simplify things even if (per @michael-s-molina's comment) this breaks compatibility.

Interesting. I'll add this comment to the card to help the discussion.

# without roles take a look at:
#
# - https://github.com/apache/superset/pull/24350#discussion_r1225061550
# - https://github.com/apache/superset/pull/17511#issuecomment-975870169
#
not dashboard.published
or not dashboard.datasources
or any(
self.can_access_datasource(datasource)
for datasource in dashboard.datasources
)
):
return

Check warning on line 2039 in superset/security/manager.py

View check run for this annotation

Codecov / codecov/patch

superset/security/manager.py#L2039

Added line #L2039 was not covered by tests

raise DashboardAccessDeniedError()

Expand Down
29 changes: 29 additions & 0 deletions tests/integration_tests/security/guest_token_security_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,32 @@ def test_raise_for_dashboard_access_as_unauthorized_guest(self):

with self.assertRaises(DashboardAccessDeniedError):
security_manager.raise_for_dashboard_access(self.dash)

def test_raise_for_dashboard_access_as_guest_no_rbac(self):
"""
Test that guest account has no access to other dashboards.

A bug in the ``raise_for_dashboard_access`` logic allowed the guest user to
fetch data from other dashboards, as long as the other dashboard:

- was not embedded AND
- was not published OR
- had at least 1 datasource that the user had access to.

"""
g.user = self.unauthorized_guest

# Create a draft dashboard that is not embedded
dash = Dashboard()
dash.dashboard_title = "My Dashboard"
dash.owners = []
dash.slices = []
dash.published = False
db.session.add(dash)
db.session.commit()

with self.assertRaises(DashboardAccessDeniedError):
security_manager.raise_for_dashboard_access(dash)

db.session.delete(dash)
db.session.commit()
Loading