Skip to content

fix: Refactor ownership checks - #20499

Merged
john-bodley merged 1 commit into
apache:masterfrom
john-bodley:john-bodley--refactor-check-ownership
Jul 7, 2022
Merged

fix: Refactor ownership checks#20499
john-bodley merged 1 commit into
apache:masterfrom
john-bodley:john-bodley--refactor-check-ownership

Conversation

@john-bodley

@john-bodley john-bodley commented Jun 25, 2022

Copy link
Copy Markdown
Member

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_access method 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 always g.user and thus continuing my crusade to simplify the whole user space (removing passing g.user all 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 the override_user context manager.

The TL;DR of this PR:

  1. Removes the actor construct across all the DAO models.
  2. Refactors check_ownership which either raised or returned a bool into the raise_for_owernship and is_owner methods—the later being a wrapper of the former. This helps to improve code readability, i.e., it is apparent that raise_for_owernship raises. These methods have been moved to the security manager to allow for customization.
  3. Removes duplicate methods, i.e., whether a user is an admin or owner.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

TESTING INSTRUCTIONS

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@john-bodley
john-bodley marked this pull request as ready for review June 25, 2022 03:00
@codecov

codecov Bot commented Jun 25, 2022

Copy link
Copy Markdown

Codecov Report

Merging #20499 (844a8b0) into master (f910958) will decrease coverage by 0.23%.
The diff coverage is 88.58%.

❗ Current head 844a8b0 differs from pull request most recent head 823b9d3. Consider uploading reports for the commit 823b9d3 to get more accurate results

@@            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     
Flag Coverage Δ
hive ?
mysql 82.33% <93.37%> (-0.06%) ⬇️
postgres 82.40% <93.37%> (-0.06%) ⬇️
presto ?
python 82.48% <93.37%> (-0.41%) ⬇️
sqlite 82.19% <93.37%> (-0.06%) ⬇️
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...frontend/src/components/ImportModal/ErrorAlert.tsx 33.33% <ø> (ø)
superset-frontend/src/views/CRUD/hooks.ts 46.56% <ø> (ø)
superset/dashboards/permalink/commands/create.py 88.88% <ø> (-0.77%) ⬇️
superset/datasets/dao.py 94.11% <ø> (+0.54%) ⬆️
superset/explore/form_data/commands/get.py 90.90% <ø> (-0.27%) ⬇️
superset/explore/form_data/commands/parameters.py 100.00% <ø> (ø)
superset/temporary_cache/commands/parameters.py 100.00% <ø> (ø)
superset/views/base.py 75.36% <ø> (-1.36%) ⬇️
superset/views/dashboard/views.py 66.27% <0.00%> (+0.76%) ⬆️
superset/views/utils.py 80.84% <ø> (-0.22%) ⬇️
... and 103 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f910958...823b9d3. Read the comment docs.

@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch 12 times, most recently from 9d16076 to 80d717b Compare June 25, 2022 23:37
Comment thread superset/security/manager.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@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.

@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch from 80d717b to f71f182 Compare July 5, 2022 21:15
@john-bodley

Copy link
Copy Markdown
Member Author

@eschutho I've addressed your question and thus this should be ready for re-review. @dpgaspar would you mind also reviewing said change given that it updates the DAO logic that you and @villebro authored.

@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch 4 times, most recently from 88c5910 to 91c9b14 Compare July 6, 2022 17:32
@john-bodley
john-bodley requested a review from eschutho July 6, 2022 17:33
@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch 3 times, most recently from d0b4273 to e220a4c Compare July 6, 2022 21:50
@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch from e220a4c to 560ff4a Compare July 6, 2022 23:18
@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch 3 times, most recently from 50b921d to 6aa4855 Compare July 7, 2022 02:07
@john-bodley
john-bodley requested a review from ktmud July 7, 2022 02:50

@ktmud ktmud left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@dpgaspar dpgaspar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Comment thread superset/security/manager.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: db.session.query(resource.__class__).filter_by(id=resource.id).one_or_none() or db.session.query(resource.__class__).get(resource.id)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@dpgaspar I agree with the one_or_none. This was a copy-and-paste from check_ownership but I'll make the update.

@john-bodley

Copy link
Copy Markdown
Member Author

@dpgaspar

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?

I don't disagree in terms of lose of flexibility, however the g.user is so engrained in the entire application workflow that we would need a very major refactor in order to support this. Per the PR description the actor is somewhat misleading given that some checks are in relation to the specified actor whereas others (like is_user_admin et al.) are based on g.user.

@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch from 6aa4855 to 7f71252 Compare July 7, 2022 16:47
@john-bodley
john-bodley force-pushed the john-bodley--refactor-check-ownership branch from 7f71252 to 823b9d3 Compare July 7, 2022 16:59
@john-bodley
john-bodley merged commit f0ca158 into apache:master Jul 7, 2022
@john-bodley
john-bodley deleted the john-bodley--refactor-check-ownership branch July 7, 2022 18:04
akshatsri pushed a commit to charan1314/superset that referenced this pull request Jul 19, 2022
Co-authored-by: John Bodley <john.bodley@airbnb.com>
@john-bodley john-bodley mentioned this pull request Oct 25, 2022
9 tasks
@john-bodley john-bodley mentioned this pull request Feb 16, 2024
9 tasks
@mistercrunch mistercrunch added 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 2.1.0 First shipped in 2.1.0 labels Mar 13, 2024
qfcwell pushed a commit to qfcwell/superset that referenced this pull request May 12, 2026
Co-authored-by: John Bodley <john.bodley@airbnb.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 13, 2026
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>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 13, 2026
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>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 14, 2026
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>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 18, 2026
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>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 25, 2026
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>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 28, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels size/XXL 🚢 2.1.0 First shipped in 2.1.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants