-
Notifications
You must be signed in to change notification settings - Fork 14.2k
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: Address dashboard permission regression in #23586 #24350
Conversation
@@ -114,27 +111,3 @@ def wrapped(*args: Any, **kwargs: Any) -> Any: | |||
|
|||
def on_security_exception(self: Any, ex: Exception) -> Response: | |||
return self.response(403, **{"message": utils.error_msg_from_exception(ex)}) | |||
|
|||
|
|||
# noinspection PyPackageRequirements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This decorator seemed superfluous as said logic could be handled elsewhere.
if self.is_admin() or self.is_owner(dashboard): | ||
return | ||
|
||
# RBAC and legacy (datasource inferred) access controls. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TL;DR is this check now includes logic for the legacy access controls which were based (for right or wrong) around the notion that a user has access to the dashboard if they can access at least one of the underlying datasets which backs the dashboard. Said logic previously resided in the dashboard view.
# pylint: disable=import-outside-toplevel | ||
from superset import is_feature_enabled | ||
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError | ||
|
||
def has_rbac_access() -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic has been inlined below.
# pylint: disable=import-outside-toplevel | ||
from superset import is_feature_enabled | ||
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError | ||
|
||
def has_rbac_access() -> bool: | ||
if not is_feature_enabled("DASHBOARD_RBAC") or not dashboard.roles: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a tad hard to digest, but it essence is checking if RBAC is disabled. A more digestible form (used below) to check if RBAC is enabled is,
if is_feature_enabled("DASHBOARD_RBAC") and dashboard.roles:
...
can_access = ( | ||
self.is_admin() | ||
or self.is_owner(dashboard) | ||
or (dashboard.published and has_rbac_access()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm struggling to grok how/why the published state alters things. Note the challenge is logic outlined here isn't the entire corpus of rules from an access standpoint given that the view also had additional (potentially conflicting) logic based on whether the user had access to the underlying datasets.
Personally I feel the access rubric is already quite complex and rather difficult to grok as it pertains to embedded dashboards and the like. Additionally I'm not sure I agree with the logic with regards to RBAC when no roles are defined. Personally this feels like one shouldn't strictly have access as opposed to falling back to the legacy logic. Do dashboard owners et al. grok how this works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally this feels like one shouldn't strictly have access as opposed to falling back to the legacy logic. Do dashboard owners et al. grok how this works?
The reason why it's like this is because otherwise it's impossible to support both regular RBAC and "dashboard RBAC" on the same running instance. I know it may be slightly confusing, but it's clearly documented, both in the modal and the docs, so changing this would be a pretty substantial breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@villebro is the desired end state to support only one or do you perceive both coexisting for some time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@john-bodley I kind of have a SIP brewing for a more universal object level RBAC model. I'll circulate it with you once I have the main design ready, as I'm also curious to hear your thoughts on it.
ecc32b9
to
72017e2
Compare
Codecov Report
@@ Coverage Diff @@
## master #24350 +/- ##
==========================================
- Coverage 69.09% 69.07% -0.02%
==========================================
Files 1903 1903
Lines 74608 74585 -23
Branches 8107 8107
==========================================
- Hits 51550 51523 -27
- Misses 20947 20951 +4
Partials 2111 2111
Flags with carried forward coverage won't be shown. Click here to find out more.
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
self.is_admin() | ||
or self.is_owner(dashboard) | ||
or (dashboard.published and has_rbac_access()) | ||
or (not dashboard.published and not dashboard.roles) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't dashboards roles only a thing if RBAC is enabled?
2642b41
to
65ca7ed
Compare
@@ -506,43 +463,6 @@ def test_get_dashboard_no_data_access(self): | |||
db.session.delete(dashboard) | |||
db.session.commit() | |||
|
|||
def test_get_draft_dashboard_without_roles_by_uuid(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to RBAC tests given dashboard roles are only associated with RBAC.
65ca7ed
to
aeb584b
Compare
""" | ||
|
||
dashboard = Dashboard.get(dashboard_id_or_slug) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is concerning that this is different than the DashboardDAO.get_by_id_or_slug and that the DAO logic differs (from a filter perspective) whether you pass in a UUID versus a slug/ID.
Personally the UUID logic is just adding more complexity to an already partially intractable problem in terms of understanding (and thus enforcing) the security model. Adhering to the KISS principle in terms of the security model will actually make the service more secure as it'll be easier to grok and enforce.
flash(DashboardAccessDeniedError.message, "danger") | ||
return redirect(DASHBOARD_LIST_URL) | ||
|
||
dash_edit_perm = security_manager.is_owner( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is inlined later for improved readability.
) | ||
|
||
bootstrap_data = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is inlined later.
aeb584b
to
9bba03b
Compare
@@ -234,3 +235,4 @@ def test_get_dashboards_api_no_data_access(self): | |||
self.assert200(rv) | |||
data = json.loads(rv.data.decode("utf-8")) | |||
self.assertEqual(0, data["count"]) | |||
DashboardDAO.delete(dashboard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously this was non-idempotent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice 👍 One more down, I wonder how many to go..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, this really cleans up this logic. As there's clearly a need for more advanced RBAC models to complement the default "datasource centric" RBAC model (I believe #10408 which introduced Dashboard RBAC is the most liked SIP of all time), it may be a good idea to consider improving/clarifying this functionality when we start considering breaking changes for 4.0. I'd personally like to see the possibility of also separating charts from the datasource permission model, so that one could control who can see certain charts, irrespective of which datasets they're referencing.
@@ -234,3 +235,4 @@ def test_get_dashboards_api_no_data_access(self): | |||
self.assert200(rv) | |||
data = json.loads(rv.data.decode("utf-8")) | |||
self.assertEqual(0, data["count"]) | |||
DashboardDAO.delete(dashboard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice 👍 One more down, I wonder how many to go..
can_access = ( | ||
self.is_admin() | ||
or self.is_owner(dashboard) | ||
or (dashboard.published and has_rbac_access()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally this feels like one shouldn't strictly have access as opposed to falling back to the legacy logic. Do dashboard owners et al. grok how this works?
The reason why it's like this is because otherwise it's impossible to support both regular RBAC and "dashboard RBAC" on the same running instance. I know it may be slightly confusing, but it's clearly documented, both in the modal and the docs, so changing this would be a pretty substantial breaking change.
@villebro If you already have something in mind, you can add cards to the Punt to 4.0 column in our board. |
SUMMARY
This PR addresses an issue in #23586 where previously the necessary dashboard access checks were a combination of logic which resided in the security manager as well as the view with conflicting logic. This was problematic say if a user was deemed to have access to a dashboard (from a security perspective) but was then denied within the view which relied on additional dataset access checks meaning it was near impossible to bypass with custom security logic.
This PR simply moves the entirety of the permission checks to the security manager.
BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
TESTING INSTRUCTIONS
CI.
ADDITIONAL INFORMATION