fix: Refactor ownership checks - #20499
Conversation
Codecov Report
@@ Coverage Diff @@
## master #20499 +/- ##
==========================================
- Coverage 66.82% 66.59% -0.24%
==========================================
Files 1752 1751 -1
Lines 65620 65504 -116
Branches 6938 6940 +2
==========================================
- Hits 43853 43620 -233
- Misses 20007 20122 +115
- Partials 1760 1762 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
9d16076 to
80d717b
Compare
There was a problem hiding this comment.
I was comparing this method to the old one which pulled out the original object from the db rather than use the one that was passed in. Could the resource here come from user input rather than the db? I'm thinking of the scenario where a non-owner might try to grant themselves ownership by adding themselves to the list of owners on a resource. Since it's not making a db call to the original resource could we be allowing them to define their own set of owners? One example perhaps is the pre_update hook for the SliceModelView.
There was a problem hiding this comment.
@eschutho apologies for the delay in responding as I was away on PTO. I think the point you raise is a good call and I'll revert the logic.
As a side note I think a lot of the pre_update logic is possibly obsolete with the DAO model—which merely complicates the code base—and we likely could deprecate some of that. I'm planning on authoring another PR to address said issue.
80d717b to
f71f182
Compare
88c5910 to
91c9b14
Compare
d0b4273 to
e220a4c
Compare
e220a4c to
560ff4a
Compare
50b921d to
6aa4855
Compare
dpgaspar
left a comment
There was a problem hiding this comment.
This does shave the Yak a bit more ;)
On the other hand we will loose some future flexibility if we would ever wanted to use DAO or commands with different users, celery context for example?
There was a problem hiding this comment.
nit: db.session.query(resource.__class__).filter_by(id=resource.id).one_or_none() or db.session.query(resource.__class__).get(resource.id)
There was a problem hiding this comment.
@dpgaspar I agree with the one_or_none. This was a copy-and-paste from check_ownership but I'll make the update.
I don't disagree in terms of lose of flexibility, however the |
6aa4855 to
7f71252
Compare
7f71252 to
823b9d3
Compare
Co-authored-by: John Bodley <john.bodley@airbnb.com>
Co-authored-by: John Bodley <john.bodley@airbnb.com>
Previously, BaseRestoreCommand.validate wrapped its raise_for_ownership call with g.skip_visibility_filter=True so the function's internal re-query of the resource could see soft-deleted rows. That worked but left the soft-delete coupling visible at every restore-command site, required dedicated tests for the bypass contract, and added a follow-up ticket (sc-106314) to consolidate the boilerplate. Move the bypass inside raise_for_ownership itself instead. The function becomes soft-delete-aware: its internal re-query always sees the row the caller passed, including when that row is soft-deleted. The bypass is scoped narrowly to the re-query (try/finally restores the previous value) so it doesn't leak past the function. Net effect for callers: - BaseRestoreCommand.validate goes back to a plain raise_for_ownership(model) call. - Every other caller is unaffected — their resources weren't soft-deleted, the re-query wasn't being filtered, the bypass is a no-op for them. The change in security/manager.py is 12 lines (try/finally wrapper + import) and surgical. It preserves the defense-in-depth value of the re-query (defense against form-data tampering of owners, see PR apache#20499 and the original 2016 commit) while making the function correct for the soft-delete case. Removed - The 15-line try/finally bypass wrapper in BaseRestoreCommand.validate - The 'from flask import g' and SKIP_VISIBILITY_FILTER imports in commands/restore.py - tests/unit_tests/commands/test_restore.py (the test file specifically exercised the bypass contract that no longer lives in BaseRestoreCommand) Updated - Listener docstring in models/helpers.py to point at the new canonical caller of the per-request flag (raise_for_ownership itself). - Bypass primer to reflect that the per-request bypass is now mostly an internal mechanism; new external callers should be rare and justified. Follow-up tickets to be updated separately - sc-106314 (context-manager refactor) becomes superseded; only one caller of the bypass remains, and that one is internal. - sc-106319 (remove the defensive re-query entirely) drops from 'fixes the soft-delete coupling' to 'pure perf optimization, optional.' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, BaseRestoreCommand.validate wrapped its raise_for_ownership call with g.skip_visibility_filter=True so the function's internal re-query of the resource could see soft-deleted rows. That worked but left the soft-delete coupling visible at every restore-command site, required dedicated tests for the bypass contract, and added a follow-up ticket (sc-106314) to consolidate the boilerplate. Move the bypass inside raise_for_ownership itself instead. The function becomes soft-delete-aware: its internal re-query always sees the row the caller passed, including when that row is soft-deleted. The bypass is scoped narrowly to the re-query (try/finally restores the previous value) so it doesn't leak past the function. Net effect for callers: - BaseRestoreCommand.validate goes back to a plain raise_for_ownership(model) call. - Every other caller is unaffected — their resources weren't soft-deleted, the re-query wasn't being filtered, the bypass is a no-op for them. The change in security/manager.py is 12 lines (try/finally wrapper + import) and surgical. It preserves the defense-in-depth value of the re-query (defense against form-data tampering of owners, see PR apache#20499 and the original 2016 commit) while making the function correct for the soft-delete case. Removed - The 15-line try/finally bypass wrapper in BaseRestoreCommand.validate - The 'from flask import g' and SKIP_VISIBILITY_FILTER imports in commands/restore.py - tests/unit_tests/commands/test_restore.py (the test file specifically exercised the bypass contract that no longer lives in BaseRestoreCommand) Updated - Listener docstring in models/helpers.py to point at the new canonical caller of the per-request flag (raise_for_ownership itself). - Bypass primer to reflect that the per-request bypass is now mostly an internal mechanism; new external callers should be rare and justified. Follow-up tickets to be updated separately - sc-106314 (context-manager refactor) becomes superseded; only one caller of the bypass remains, and that one is internal. - sc-106319 (remove the defensive re-query entirely) drops from 'fixes the soft-delete coupling' to 'pure perf optimization, optional.' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, BaseRestoreCommand.validate wrapped its raise_for_ownership call with g.skip_visibility_filter=True so the function's internal re-query of the resource could see soft-deleted rows. That worked but left the soft-delete coupling visible at every restore-command site, required dedicated tests for the bypass contract, and added a follow-up ticket (sc-106314) to consolidate the boilerplate. Move the bypass inside raise_for_ownership itself instead. The function becomes soft-delete-aware: its internal re-query always sees the row the caller passed, including when that row is soft-deleted. The bypass is scoped narrowly to the re-query (try/finally restores the previous value) so it doesn't leak past the function. Net effect for callers: - BaseRestoreCommand.validate goes back to a plain raise_for_ownership(model) call. - Every other caller is unaffected — their resources weren't soft-deleted, the re-query wasn't being filtered, the bypass is a no-op for them. The change in security/manager.py is 12 lines (try/finally wrapper + import) and surgical. It preserves the defense-in-depth value of the re-query (defense against form-data tampering of owners, see PR apache#20499 and the original 2016 commit) while making the function correct for the soft-delete case. Removed - The 15-line try/finally bypass wrapper in BaseRestoreCommand.validate - The 'from flask import g' and SKIP_VISIBILITY_FILTER imports in commands/restore.py - tests/unit_tests/commands/test_restore.py (the test file specifically exercised the bypass contract that no longer lives in BaseRestoreCommand) Updated - Listener docstring in models/helpers.py to point at the new canonical caller of the per-request flag (raise_for_ownership itself). - Bypass primer to reflect that the per-request bypass is now mostly an internal mechanism; new external callers should be rare and justified. Follow-up tickets to be updated separately - sc-106314 (context-manager refactor) becomes superseded; only one caller of the bypass remains, and that one is internal. - sc-106319 (remove the defensive re-query entirely) drops from 'fixes the soft-delete coupling' to 'pure perf optimization, optional.' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, BaseRestoreCommand.validate wrapped its raise_for_ownership call with g.skip_visibility_filter=True so the function's internal re-query of the resource could see soft-deleted rows. That worked but left the soft-delete coupling visible at every restore-command site, required dedicated tests for the bypass contract, and added a follow-up ticket (sc-106314) to consolidate the boilerplate. Move the bypass inside raise_for_ownership itself instead. The function becomes soft-delete-aware: its internal re-query always sees the row the caller passed, including when that row is soft-deleted. The bypass is scoped narrowly to the re-query (try/finally restores the previous value) so it doesn't leak past the function. Net effect for callers: - BaseRestoreCommand.validate goes back to a plain raise_for_ownership(model) call. - Every other caller is unaffected — their resources weren't soft-deleted, the re-query wasn't being filtered, the bypass is a no-op for them. The change in security/manager.py is 12 lines (try/finally wrapper + import) and surgical. It preserves the defense-in-depth value of the re-query (defense against form-data tampering of owners, see PR apache#20499 and the original 2016 commit) while making the function correct for the soft-delete case. Removed - The 15-line try/finally bypass wrapper in BaseRestoreCommand.validate - The 'from flask import g' and SKIP_VISIBILITY_FILTER imports in commands/restore.py - tests/unit_tests/commands/test_restore.py (the test file specifically exercised the bypass contract that no longer lives in BaseRestoreCommand) Updated - Listener docstring in models/helpers.py to point at the new canonical caller of the per-request flag (raise_for_ownership itself). - Bypass primer to reflect that the per-request bypass is now mostly an internal mechanism; new external callers should be rare and justified. Follow-up tickets to be updated separately - sc-106314 (context-manager refactor) becomes superseded; only one caller of the bypass remains, and that one is internal. - sc-106319 (remove the defensive re-query entirely) drops from 'fixes the soft-delete coupling' to 'pure perf optimization, optional.' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, BaseRestoreCommand.validate wrapped its raise_for_ownership call with g.skip_visibility_filter=True so the function's internal re-query of the resource could see soft-deleted rows. That worked but left the soft-delete coupling visible at every restore-command site, required dedicated tests for the bypass contract, and added a follow-up ticket (sc-106314) to consolidate the boilerplate. Move the bypass inside raise_for_ownership itself instead. The function becomes soft-delete-aware: its internal re-query always sees the row the caller passed, including when that row is soft-deleted. The bypass is scoped narrowly to the re-query (try/finally restores the previous value) so it doesn't leak past the function. Net effect for callers: - BaseRestoreCommand.validate goes back to a plain raise_for_ownership(model) call. - Every other caller is unaffected — their resources weren't soft-deleted, the re-query wasn't being filtered, the bypass is a no-op for them. The change in security/manager.py is 12 lines (try/finally wrapper + import) and surgical. It preserves the defense-in-depth value of the re-query (defense against form-data tampering of owners, see PR apache#20499 and the original 2016 commit) while making the function correct for the soft-delete case. Removed - The 15-line try/finally bypass wrapper in BaseRestoreCommand.validate - The 'from flask import g' and SKIP_VISIBILITY_FILTER imports in commands/restore.py - tests/unit_tests/commands/test_restore.py (the test file specifically exercised the bypass contract that no longer lives in BaseRestoreCommand) Updated - Listener docstring in models/helpers.py to point at the new canonical caller of the per-request flag (raise_for_ownership itself). - Bypass primer to reflect that the per-request bypass is now mostly an internal mechanism; new external callers should be rare and justified. Follow-up tickets to be updated separately - sc-106314 (context-manager refactor) becomes superseded; only one caller of the bypass remains, and that one is internal. - sc-106319 (remove the defensive re-query entirely) drops from 'fixes the soft-delete coupling' to 'pure perf optimization, optional.' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, BaseRestoreCommand.validate wrapped its raise_for_ownership call with g.skip_visibility_filter=True so the function's internal re-query of the resource could see soft-deleted rows. That worked but left the soft-delete coupling visible at every restore-command site, required dedicated tests for the bypass contract, and added a follow-up ticket (sc-106314) to consolidate the boilerplate. Move the bypass inside raise_for_ownership itself instead. The function becomes soft-delete-aware: its internal re-query always sees the row the caller passed, including when that row is soft-deleted. The bypass is scoped narrowly to the re-query (try/finally restores the previous value) so it doesn't leak past the function. Net effect for callers: - BaseRestoreCommand.validate goes back to a plain raise_for_ownership(model) call. - Every other caller is unaffected — their resources weren't soft-deleted, the re-query wasn't being filtered, the bypass is a no-op for them. The change in security/manager.py is 12 lines (try/finally wrapper + import) and surgical. It preserves the defense-in-depth value of the re-query (defense against form-data tampering of owners, see PR apache#20499 and the original 2016 commit) while making the function correct for the soft-delete case. Removed - The 15-line try/finally bypass wrapper in BaseRestoreCommand.validate - The 'from flask import g' and SKIP_VISIBILITY_FILTER imports in commands/restore.py - tests/unit_tests/commands/test_restore.py (the test file specifically exercised the bypass contract that no longer lives in BaseRestoreCommand) Updated - Listener docstring in models/helpers.py to point at the new canonical caller of the per-request flag (raise_for_ownership itself). - Bypass primer to reflect that the per-request bypass is now mostly an internal mechanism; new external callers should be rare and justified. Follow-up tickets to be updated separately - sc-106314 (context-manager refactor) becomes superseded; only one caller of the bypass remains, and that one is internal. - sc-106319 (remove the defensive re-query entirely) drops from 'fixes the soft-delete coupling' to 'pure perf optimization, optional.' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SUMMARY
Yak shaving at its finest. Originally I drafted this PR to fix an issue with inconsistencies between ownership checks as described in this Slack thread. BTW this PR does not address said issue—I believe we only need a migration for legacy charts/datasets.
Things quickly spiraled out of control as I realized that the DAO
check_accessmethod checks whether the actor is an owner alongside a check whether the current user is an admin—this clearly is wrong, i.e., both checks should be for the same user. Digging further I realize that the actor is alwaysg.userand thus continuing my crusade to simplify the whole user space (removing passingg.userall around the place) I opted to fully deprecate the actor concept—which touched a huge swatch of files. The main reason being is that Superset is really only configured to work with the logged in user and overrides should be done—if needed—via theoverride_usercontext manager.The TL;DR of this PR:
actorconstruct across all the DAO models.check_ownershipwhich either raised or returned aboolinto theraise_for_owernshipandis_ownermethods—the later being a wrapper of the former. This helps to improve code readability, i.e., it is apparent thatraise_for_owernshipraises. These methods have been moved to the security manager to allow for customization.BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
TESTING INSTRUCTIONS
ADDITIONAL INFORMATION