Skip to content

fix(models): enforce one OAuth2 token per user+database - #42897

Open
rusackas wants to merge 2 commits into
apache:masterfrom
rusackas:fix/oauth2-token-uniqueness
Open

fix(models): enforce one OAuth2 token per user+database#42897
rusackas wants to merge 2 commits into
apache:masterfrom
rusackas:fix/oauth2-token-uniqueness

Conversation

@rusackas

@rusackas rusackas commented Aug 7, 2026

Copy link
Copy Markdown
Member

SUMMARY

database_user_oauth2_tokens only ever carries one live token per (user_id, database_id) pair -- OAuth2StoreTokenCommand always deletes any existing token before storing a new one -- but that invariant was only enforced in application code, via a plain (non-unique) index (idx_user_id_database_id). A race between two concurrent OAuth2 callbacks for the same user+database can leave duplicate rows behind, and nothing downstream (get_oauth2_access_token, refresh_oauth2_token) picks a deterministic one -- both do .filter_by(user_id=..., database_id=...).one_or_none(), which raises MultipleResultsFound if duplicates exist.

This is a follow-up to #42211, which fixed an unrelated purge_oauth2_tokens filter bug on the same table. During review there, Vitor-Avila asked whether multiple tokens per user+db should be blocked, and rusackas agreed it was worth enforcing but out of scope for that fix:

Fair question, but that's pre-existing behavior separate from this bug. There's no unique constraint on user_id + database_id today, just an index for lookups, so nothing stops multiple tokens from piling up. Worth a follow-up if we want to enforce one token per user per db, but I don't think it should block this fix.

This PR closes that gap by making the index unique.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable -- internal schema/model change, no UI.

TESTING INSTRUCTIONS

pytest tests/unit_tests/models/core_test.py -k oauth2 -v
pytest tests/unit_tests/migrations/test_enforce_oauth2_token_uniqueness.py -v
  • test_oauth2_tokens_unique_per_user_and_database (new): confirms a second row for the same (user_id, database_id) pair raises IntegrityError.
  • test_purge_oauth2_tokens_scoped_by_database_id (existing): updated so its PK-drift setup uses distinct users instead of repeating one user against the same database, since the latter now violates the new constraint.
  • tests/unit_tests/migrations/test_enforce_oauth2_token_uniqueness.py (new): runs the migration's upgrade()/downgrade() against an in-memory SQLite engine seeded with pre-existing duplicate rows, and checks the dedupe-then-uniquify pre-flight, the resulting index's unique flag, that new duplicates are rejected post-upgrade, and that downgrade() restores a plain index without reversing the dedupe deletions.

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: single-table DELETE (only affects pre-existing duplicate rows, expected to be rare-to-none in practice) plus an index rebuild on database_user_oauth2_tokens, which is typically small. No measurable downtime expected for realistic table sizes.
  • Introduces new feature or API
  • Removes existing feature or API

…follow-up)

`OAuth2StoreTokenCommand` always deletes any existing token before
storing a new one, so `database_user_oauth2_tokens` is only ever meant
to carry a single row per (user_id, database_id) pair -- but that
invariant was only enforced in application code via a plain
(non-unique) lookup index. A race between two concurrent OAuth2
callbacks for the same user+database can leave duplicate rows behind,
and nothing downstream picks a deterministic one of them.

Flagged as an explicit follow-up during review of apache#42211 (which fixed
an unrelated purge_oauth2_tokens filter bug on the same table):
Vitor-Avila asked whether multiple tokens per user+db should be
blocked, and rusackas agreed it was worth enforcing but out of scope
for that fix.

Adds a migration that dedupes any pre-existing duplicate rows (keeping
the highest id, i.e. the most recently issued token) before flipping
the index to unique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dosubot dosubot Bot added authentication:sso Single Sign On data:databases Related to database configurations and connections labels Aug 7, 2026
@bito-code-review

bito-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #e646a5

Actionable Suggestions - 0
Review Details
  • Files reviewed - 4 · Commit Range: 0f26a0f..0f26a0f
    • superset/migrations/versions/2026-08-07_09-00_da0e3f0081bf_enforce_oauth2_token_uniqueness.py
    • superset/models/core.py
    • tests/unit_tests/migrations/test_enforce_oauth2_token_uniqueness.py
    • tests/unit_tests/models/core_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@github-actions github-actions Bot added the risk:db-migration PRs that require a DB migration label Aug 7, 2026
@bito-code-review

Copy link
Copy Markdown
Contributor

The review comment correctly identifies a MySQL limitation where a table cannot be modified (deleted from) while being read in a subquery. To resolve this in a database-agnostic way, you should fetch the IDs to keep into a Python list first, then perform the deletion using that list.

Here is the corrected upgrade function for superset/migrations/versions/2026-08-07_09-00_da0e3f0081bf_enforce_oauth2_token_uniqueness.py:

def upgrade() -> None:
    bind = op.get_bind()
    metadata = sa.MetaData()
    table = sa.Table(TABLE_NAME, metadata, autoload_with=bind)

    # Fetch IDs to keep into memory to avoid MySQL 1093 error
    keep_ids = bind.execute(
        select(func.max(table.c.id))
        .group_by(table.c.user_id, table.c.database_id)
    ).scalars().all()

    bind.execute(table.delete().where(table.c.id.notin_(keep_ids)))

    drop_index(TABLE_NAME, INDEX_NAME)
    create_index(TABLE_NAME, INDEX_NAME, ["user_id", "database_id"], unique=True)

superset/migrations/versions/2026-08-07_09-00_da0e3f0081bf_enforce_oauth2_token_uniqueness.py

# Fetch IDs to keep into memory to avoid MySQL 1093 error
    keep_ids = bind.execute(
        select(func.max(table.c.id))
        .group_by(table.c.user_id, table.c.database_id)
    ).scalars().all()

    bind.execute(table.delete().where(table.c.id.notin_(keep_ids)))

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.38%. Comparing base (4c894af) to head (f0e3254).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
superset/commands/database/oauth2.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42897      +/-   ##
==========================================
- Coverage   66.38%   66.38%   -0.01%     
==========================================
  Files        2857     2857              
  Lines      161133   161165      +32     
  Branches    37064    37074      +10     
==========================================
+ Hits       106967   106988      +21     
- Misses      52147    52155       +8     
- Partials     2019     2022       +3     
Flag Coverage Δ
hive 38.24% <66.66%> (-0.01%) ⬇️
mysql 57.78% <66.66%> (-0.01%) ⬇️
postgres 57.83% <66.66%> (+<0.01%) ⬆️
presto 40.20% <66.66%> (-0.01%) ⬇️
python 59.23% <66.66%> (+<0.01%) ⬆️
sqlite 57.45% <66.66%> (-0.01%) ⬇️
unit 100.00% <ø> (ø)

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…2 uniqueness migration

- OAuth2StoreTokenCommand deleted the old token then added the new one
  in the same flush; the unit of work emits INSERTs before DELETEs,
  which tripped the new (user_id, database_id) unique index on refresh.
  Flush right after the delete so the old row is gone before the new
  one lands.
- The migration's dedupe DELETE subqueried from the same table, which
  MySQL rejects (error 1093); read duplicate ids in Python instead.
- Dropping the old index outright also failed on MySQL (error 1553)
  since it was the only index covering the user_id FK; create the new
  unique index under a temporary name first so an FK-satisfying index
  always exists, then swap it into place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #0bce87

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 0f26a0f..f0e3254
    • superset/commands/database/oauth2.py
    • superset/migrations/versions/2026-08-07_09-00_da0e3f0081bf_enforce_oauth2_token_uniqueness.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authentication:sso Single Sign On data:databases Related to database configurations and connections risk:db-migration PRs that require a DB migration size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants