Skip to content

feat(auth): implement password change API for AUTH_DB authentication#39469

Open
shantanukhond wants to merge 42 commits into
apache:masterfrom
shantanukhond:feat/auth-db-password-and-audit
Open

feat(auth): implement password change API for AUTH_DB authentication#39469
shantanukhond wants to merge 42 commits into
apache:masterfrom
shantanukhond:feat/auth-db-password-and-audit

Conversation

@shantanukhond

@shantanukhond shantanukhond commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Pillar 1 of SIP-201 / #37927: self-service password change for AUTH_DB users.

  • PUT /api/v1/me/password (AUTH_DB only) accepts current_password, new_password, and confirm_password; verifies the current password, validates the new password against AUTH_DB_CONFIG, hashes with bcrypt (default) or argon2, clears password_must_change on success, rotates a per-user session auth stamp (invalidating other browser sessions), and re-establishes the current session safely.
  • GET /api/v1/me/password/policy returns non-secret policy options for real-time UI validation.
  • PUT /api/v1/me/ still updates name fields but rejects password when AUTH_TYPE is AUTH_DB (breaking change).
  • AUTH_DB_CONFIG defines minimum length, required character classes, common-password blocklist, hash algorithm, and rate limits; validation is shared between backend enforcement and frontend helpers (generateAuthDbPassword, policy indicator).
  • UI: User info → Reset my password modal with confirm field, policy indicator, and optional password generator.
  • Documentation and UPDATING.md describe the new flow, policy knobs, session invalidation, and the breaking PUT /api/v1/me/ behavior.
  • Tests: unit coverage for password policy, hashing, and session stamps; integration coverage for password/profile/policy APIs under AUTH_DB.
  • DB migration: user_session_auth_stamp table for cross-session invalidation.

Related: #37927
Migration standards context: SIP-59 / #13351

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Before

image

After

image image

TESTING INSTRUCTIONS

  1. Configure AUTH_TYPE = AUTH_DB (and a DB auth user). Run superset db upgrade so the new migration is applied.
  2. Self-service change: open User infoReset my password; submit with wrong current password → expect a clear error; submit with a weak new password → expect policy error; submit with valid passwords → expect success and remain logged in with the new password.
  3. Profile without password: Edit user from User info → PUT /api/v1/me/ still updates name fields; confirm password is not accepted on that endpoint for AUTH_DB.
  4. Admin reset: as an admin, open List users, use Reset password for another user; save and confirm login works with the new password.
  5. Audit: after a successful self password change, confirm a row exists in auth_audit_log (or equivalent check your environment allows).
  6. Migration: run superset db downgrade one step and superset db upgrade again in a dev database to confirm the migration is reversible (adjust if your review process requires a specific DB engine).

ADDITIONAL INFORMATION

  • Has associated issue: [SIP-201] Comprehensive Password & Credential Management Overhaul #37927
  • 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

@github-actions github-actions Bot added risk:db-migration PRs that require a DB migration api Related to the REST API doc Namespace | Anything related to documentation labels Apr 19, 2026
@dosubot dosubot Bot added authentication Related to authentication change:backend Requires changing the backend change:frontend Requires changing the frontend risk:breaking-change Issues or PRs that will introduce breaking changes labels Apr 19, 2026
@netlify

netlify Bot commented Apr 19, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit d831163
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a5a01b3fbd4fc0008b79fdc
😎 Deploy Preview https://deploy-preview-39469--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread tests/unit_tests/utils/test_auth_db_password.py
Comment thread superset/models/auth_audit_log.py Outdated
Comment thread superset/utils/auth_db_password.py
Comment thread superset-frontend/src/features/userInfo/UserInfoModal.tsx Outdated
Comment thread superset-frontend/src/features/userInfo/UserInfoModal.tsx
Comment thread superset-frontend/src/features/userInfo/UserInfoModal.tsx
Comment thread superset-frontend/src/features/users/UserListResetPasswordModal.tsx Outdated
Comment thread superset-frontend/src/utils/generateAuthDbPassword.ts Outdated

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Agent Run #6616fd

Actionable Suggestions - 1
  • superset/daos/auth_audit_log.py - 1
    • Use timezone-aware datetime instead utcnow · Line 51-51
Additional Suggestions - 3
  • docs/admin_docs/configuration/configuring-superset.mdx - 1
    • Incorrect default values path · Line 369-369
      The documentation states that defaults for AUTH_DB_CONFIG are merged from superset/config.py, but the actual defaults (AUTH_DB_DEFAULTS) are defined in superset/utils/auth_db_password.py. This inaccuracy could mislead users searching for default values. The code shows config.py imports AUTH_DB_DEFAULTS from auth_db_password.py and uses it for merging.
      Code suggestion
       @@ -369,1 +369,1 @@
      - Any keys you omit are merged with the defaults from `superset/config.py`.
      + Any keys you omit are merged with the defaults from `superset/utils/auth_db_password.py`.
  • superset/utils/auth_db_password.py - 1
    • Misleading error message · Line 121-123
      The error message for the special character requirement states 'non-alphanumeric' but the code excludes spaces from counting as special. This could mislead users into thinking spaces satisfy the requirement. Change to 'special character (not a letter, digit, or space)' for clarity.
      Code suggestion
       @@ -120,3 +120,3 @@
      -    ):
      -        errors.append(
      -            "Password must contain at least one special (non-alphanumeric) character."
      +    ):
      +        errors.append(
      +            "Password must contain at least one special character (not a letter, digit, or space)."
  • superset/daos/auth_audit_log.py - 1
    • Redundant field assignment · Line 45-51
      The `AuthAuditLog` model already defines a default value for `created_at` using `datetime.utcnow()`. Explicitly setting it in the DAO is unnecessary and can be removed to rely on the model's default, simplifying the code without changing behavior.
      Code suggestion
       @@ -45,8 +45,7 @@
      -        entry = AuthAuditLog(
      -            user_id=user_id,
      -            event_type=event_type,
      -            ip_address=ip_address,
      -            user_agent=user_agent,
      -            event_metadata=metadata,
      -            created_at=datetime.utcnow(),
      -        )
      +        entry = AuthAuditLog(
      +            user_id=user_id,
      +            event_type=event_type,
      +            ip_address=ip_address,
      +            user_agent=user_agent,
      +            event_metadata=metadata,
      +        )
Review Details
  • Files reviewed - 22 · Commit Range: a1339a4..a1339a4
    • docs/admin_docs/configuration/configuring-superset.mdx
    • superset-frontend/src/components/GeneratePasswordInputSuffix.tsx
    • superset-frontend/src/features/userInfo/UserInfoModal.tsx
    • superset-frontend/src/features/users/UserListResetPasswordModal.tsx
    • superset-frontend/src/features/users/utils.ts
    • superset-frontend/src/pages/UserInfo/index.tsx
    • superset-frontend/src/pages/UsersList/UsersList.test.tsx
    • superset-frontend/src/pages/UsersList/index.tsx
    • superset-frontend/src/utils/generateAuthDbPassword.test.ts
    • superset-frontend/src/utils/generateAuthDbPassword.ts
    • superset/config.py
    • superset/daos/auth_audit_log.py
    • superset/migrations/versions/2026-04-19_14-30_b7e4f2a891c3_add_auth_audit_log_table.py
    • superset/models/__init__.py
    • superset/models/auth_audit_log.py
    • superset/security/manager.py
    • superset/utils/auth_db_password.py
    • superset/views/users/api.py
    • superset/views/users/schemas.py
    • tests/integration_tests/users/api_tests.py
    • tests/unit_tests/daos/test_auth_audit_log_dao.py
    • tests/unit_tests/utils/test_auth_db_password.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ 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

Comment thread superset/daos/auth_audit_log.py Outdated
@codecov

codecov Bot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.62888% with 188 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.19%. Comparing base (bef03fb) to head (d831163).

Files with missing lines Patch % Lines
superset/utils/auth_session_stamp.py 68.20% 45 Missing and 17 partials ⚠️
superset/security/manager.py 34.14% 23 Missing and 4 partials ⚠️
superset/utils/auth_db_password_hash.py 53.84% 18 Missing and 6 partials ⚠️
superset/views/users/api.py 80.90% 16 Missing and 5 partials ⚠️
superset/utils/auth_db_password.py 76.62% 12 Missing and 6 partials ⚠️
...d/src/components/AuthDbPasswordPolicyIndicator.tsx 75.51% 12 Missing ⚠️
...erset-frontend/src/utils/generateAuthDbPassword.ts 90.59% 11 Missing ⚠️
...t-frontend/src/features/userInfo/UserInfoModal.tsx 83.33% 9 Missing ⚠️
...end/src/components/GeneratePasswordInputSuffix.tsx 75.00% 2 Missing ⚠️
superset/cli/reset.py 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #39469      +/-   ##
==========================================
+ Coverage   65.13%   65.19%   +0.05%     
==========================================
  Files        2753     2760       +7     
  Lines      154584   155288     +704     
  Branches    35463    35600     +137     
==========================================
+ Hits       100695   101234     +539     
- Misses      51975    52102     +127     
- Partials     1914     1952      +38     
Flag Coverage Δ
hive 39.04% <46.44%> (+0.04%) ⬆️
javascript 70.78% <85.53%> (+0.05%) ⬆️
mysql 57.81% <69.56%> (+0.08%) ⬆️
postgres 57.86% <69.56%> (+0.08%) ⬆️
presto 41.00% <46.44%> (+0.03%) ⬆️
python 59.29% <69.56%> (+0.07%) ⬆️
sqlite 57.48% <69.56%> (+0.08%) ⬆️
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.

@aminghadersohi

Copy link
Copy Markdown
Contributor

Hey @shantanukhond! 👋 This review was performed by Claude (Amin's AI assistant) — I did an independent review and then cross-checked my findings against a second AI reviewer (Codex) for additional confidence.

Key Findings (both reviewers agreed)

Blockers

  1. Session cleared before DB commit (api.py, update_my_password): The session keys are popped and login_user is called before db.session.commit(). If the commit fails, the user's session is destroyed but the password wasn't actually changed — effectively locking them out. Recommend moving session clear + login_user to after a successful commit.

  2. Audit log + password change not atomic: AuthAuditLogDAO.create() calls db.session.flush(), but if the subsequent commit fails, you end up with partial state. Wrapping the whole operation in a try/except with db.session.rollback() on failure would make this safer.

  3. Other active sessions not invalidated: The current session is cleared, but other browser sessions or remembered sessions remain valid after a password change. If the password was changed due to a compromise, the attacker's session lives on.

Medium Concerns

  1. pre_update in SupersetUserApi (manager.py): Reading from request.get_json(silent=True) directly bypasses FAB's schema layer, and raising BadRequest inside pre_update may not play well with FAB's safe decorator.

  2. No rate limiting on PUT /api/v1/me/password: The config defines login_rate_limit and reset_rate_limit but they aren't enforced on this endpoint, leaving it open to brute-force guessing of current_password.

  3. g.user may be stale after commit: After the commit, the SQLAlchemy object could be expired/detached. Should refresh the user from DB before passing to login_user.

  4. Concurrent password change race: Two concurrent requests with the same old password can both pass check_password_hash — last commit wins.

Minor

  1. datetime.utcnow() is deprecated in Python 3.12+ — use datetime.now(timezone.utc) instead (auth_audit_log.py, AuthAuditLog model).

  2. metadata physical column name conflicts with SQLAlchemy's reserved metadata attribute. Consider naming the physical column event_metadata too.

  3. Common password list duplicated between auth_db_password.py and generateAuthDbPassword.ts — these will inevitably drift. Consider making the backend authoritative.

  4. Modulo bias in secureRandomInt (generateAuthDbPassword.ts): buf[0] % maxExclusive has slight bias for non-power-of-2 values. Negligible for password gen but worth noting.

  5. Test cleanup fragility: test_put_my_password_success manually resets the password without try/finally — if an assertion fails midway, subsequent tests may break.

Overall this is a well-structured feature with good separation of concerns and solid test coverage. The session ordering issue (#1) is the most important fix. Nice work! 🙌

@sfirke

sfirke commented Apr 22, 2026

Copy link
Copy Markdown
Member

I see you referenced SIP 201: #37927 thanks for connecting the dots there and referencing an agreed-upon plan! Did you coordinate this work with @EnxDev who wrote that SIP? I wonder if they are working on this too?

@EnxDev

EnxDev commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

I see you referenced SIP 201: #37927 thanks for connecting the dots there and referencing an agreed-upon plan! Did you coordinate this work with @EnxDev who wrote that SIP? I wonder if they are working on this too?

Hi, thanks @shantanukhond for getting this started. I’m currently focused on other priorities, but as soon as I have some availability I plan to move this SIP forward. In the meantime, we can sync.

@github-actions github-actions Bot removed the doc Namespace | Anything related to documentation label May 3, 2026
@EnxDev

EnxDev commented May 3, 2026

Copy link
Copy Markdown
Contributor

Hi @shantanukhond, sharing notes from the SIP-author perspective focused on alignment rather than the implementation, which the prior review covers thoroughly.

+1 in particular to the session-clear-before-commit, atomicity, and rate-limiting points raised there.

Scope suggest splitting this PR

The change currently bundles Pillar 1 (authenticated password change) with parts of Pillar 3 (admin reset).
Given the XXL size, splitting would help reviewers and let the pillars progress independently:

  1. This PR → Pillar 1 + auth_audit_log foundation
  2. New PR → Pillar 3 admin reset

Pillar 3 — design alignment needed

The admin "set password directly" flow conflicts with the SIP.

Per Rejected Alternative 6, admins should trigger an email reset link rather than ever seeing or setting the user's password (zero-knowledge principle, since admins can leave or change roles).
The Pillar 3 PR should be reworked around the SMTP-based flow.

Pillar 1 — items still missing vs. the SIP

• Multi-session invalidation across all active sessions for the user, not only the current one.
• Persistent / remember-me token revocation after a password change.
• Password confirmation field in the form (SIP specifies current + new + confirmation).
• Real-time password strength / policy indicator as the user types.
• Audit event metadata — capture whether the change was self- or admin-initiated, and ensure source IP is populated on the row.
• password_hash_algorithm config (bcrypt/argon2) — can be deferred with a TODO if we want to keep the PR tight.

I suggest reading the linked SIP carefully before further development.

Happy to track these on the SIP side as the work progresses.

@shantanukhond

Copy link
Copy Markdown
Contributor Author

@EnxDev Thank you so much for your guidance. I will try to finish first part i.e. Pillar 1 by this week. I will keep you posted. I will also go through the SIP. Thank you.

@sadpandajoe
sadpandajoe requested review from dpgaspar and villebro May 5, 2026 17:48
@EnxDev

EnxDev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

EnxDev's Review Agent — #39469 · HEAD f0de405

request changes — two blockers: (1) the new migration forks the Alembic tree into two heads, so every DB-dependent CI job fails at db upgrade; (2) the password-change endpoint still writes plaintext credentials into the logs table.

Re-review; supersedes my prior review (HEAD 5e2a6bc). The plaintext-logging blocker below was raised there and is not addressed in this revision. The core password-change flow otherwise remains well built.

🔴 Functional

  • superset/migrations/versions/2026-05-13_12-00_c6219cac9270_add_user_session_auth_stamp_table.py:31 · Highdown_revision = "b4a3f2e1d0c9" targets a mid-graph revision, not the head. b4a3f2e1d0c9 already has a child (7c4a8d09ca37, add_deleted_at_to_slices), so this migration becomes a second child and forks the tree into two heads. flask db upgrade then aborts with Error: Multiple head revisions are present for given argument 'head' — the exact error in the failing test-sqlite / test-mysql / test-postgres / docker-build logs, which is why the whole pipeline is red before any test runs. Fix: repoint down_revision to the current single head (7c4a8d09ca37 today; re-run alembic heads after rebasing — it moves). regression test: the existing single-head CI guard passes once alembic heads returns one revision.

  • superset/views/users/api.py:318 · Highupdate_my_password is decorated with @event_logger.log_this_with_context. On context exit, log_with_context builds payload = collect_request_payload() (utils/log.py:32), which does payload.update(request.get_json()) — merging current_password, new_password, confirm_password verbatim — then calls self.log(records=[payload], …). The default EVENT_LOGGER = DBEventLogger uses records directly (utils/log.py: curated_payload is only a fallback if not records), json.dumps-ing each into Log.json and committing. So both passwords land in the metadata logs table in plaintext, on success and failure paths alike — directly contradicting this PR's own doc ("never stored in plaintext or logged", configuring-superset.mdx). Principal: any role with can_read on Log (Admin by default, but log read is commonly delegated to custom auditor roles, and logs is routinely shipped to external SIEMs/backups); leaking current_password in particular hands over a credential users often reuse elsewhere. Fix: drop log_this_with_context here and emit a manual event carrying only {user_id, action}, or scrub password-like keys in collect_request_payload. regression test: after a successful PUT /me/password, assert no Log.json row contains the new or current password.

🟡 Should-fix

  • superset/security/manager.py (reset_password) and superset/cli/reset.py — only the self-service path (_commit_user_password_change) calls bump_user_session_auth_stamp. Admin-initiated resets and the CLI don't rotate the stamp, so an admin resetting a compromised account (the case that matters) leaves the attacker's existing session valid. Bump the stamp there too, or note it as an explicit Pillar-1 follow-up.
  • superset/utils/auth_session_stamp.py:377 — a signed session with no _auth_session_stamp is adopted to the current DB stamp rather than invalidated, so sessions authenticated before this feature deploys survive one password rotation. Bounded to the rollout window (the cookie is signed, so the stamp can't be stripped by an attacker); confirm that's the intended trade-off.

🔵 Nits

  • After repointing the migration, regenerate the file — its 2026-05-13 date/Create Date predates its new parent, so the chain reads out of order.
  • sync_session_auth_stamp_on_login fires twice per login (once from on_user_login, once from the user_logged_in signal handler) — idempotent but redundant; keep one path.
  • PR title: the breaking PUT /api/v1/me/ password rejection warrants feat(auth)!: (the risk:breaking-change label is set; the ! is not).

🙌 Praise

  • _commit_user_password_change — optimistic-concurrency UPDATE … WHERE password = old_hash (→ PasswordChangeConflictError → 400), with the hash write + stamp bump + clear_password_must_change all inside one nested-safe @transaction() (verified re-entrant), cache updated only post-commit, then session rebuilt. Correct and clean.
  • bcrypt's 72-byte truncation is guarded at the hash, verify, and policy layers, and test_common_passwords_stay_in_sync_with_frontend asserts the TS blocklist equals _COMMON_PASSWORDS — exactly the drift guard this dual-validation design needs.

Verified as non-issues so they aren't re-raised: noop_user_update / get_first_user / update_user_auth_stat all exist on FAB's BaseSecurityManager (the auth_user_db override runs); AUTH_DB_FAKE_PASSWORD_HASH_CHECK uses .get(…, DEFAULT) (no KeyError); frontend digit/length checks are Unicode-aware (\p{Nd}, code-point counting) and match the backend; the change clears password_must_change and the endpoint is in _EXEMPT_ENDPOINTS.

Reviewed by EnxDev's Review Agent — @EnxDev · HEAD f0de405.

Comment thread superset/cli/reset.py
if (
not user
or not user.is_active
or not verify_auth_db_password(user.password, password)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: This direct hash check bypasses the security manager authentication flow, so failed-attempt accounting/timing-balancing logic is skipped and this command behaves differently from normal login protections. Route credential validation through the security manager auth method so lockout/rate-limit and failure bookkeeping are consistently enforced. [security]

Severity Level: Major ⚠️
- ❌ Factory reset CLI ignores AUTH_DB login failure tracking.
- ⚠️ Brute-force attempts via CLI bypass lockout protections.
Steps of Reproduction ✅
1. Invoke the Superset CLI entrypoint `superset` defined in `superset/cli/main.py:55-73`,
which auto-loads subcommands from the `superset.cli` package.

2. Ensure the factory reset command is enabled by setting
`FEATURE_FLAGS["ENABLE_FACTORY_RESET_COMMAND"] = True` so the check at
`superset/cli/reset.py:55-57` passes.

3. Run `superset factory_reset` from the CLI, enter an existing admin username and an
incorrect password at the prompts implemented in `superset/cli/reset.py:69-70`; execution
reaches the validation block at lines 72-76.

4. In `superset/cli/reset.py:72-76`, the condition `or not
verify_auth_db_password(user.password, password)` (line 75) directly checks the stored
hash and, on failure, prints "Invalid credentials" and calls `sys.exit(1)` (lines 77-78)
without ever invoking `SupersetSecurityManager.auth_user_db()` in
`superset/security/manager.py:84-115`, so the usual failure bookkeeping via
`update_user_auth_stat()` and fake-hash timing-balancing logic are skipped for this
command.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/cli/reset.py
**Line:** 75:75
**Comment:**
	*Security: This direct hash check bypasses the security manager authentication flow, so failed-attempt accounting/timing-balancing logic is skipped and this command behaves differently from normal login protections. Route credential validation through the security manager auth method so lockout/rate-limit and failure bookkeeping are consistently enforced.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +368 to +377
if sess_stamp is None or sess_stamp != db_stamp:
# Sessions authenticated before stamp tracking, or a login that did not
# persist the stamp into the signed cookie, carry no sess stamp. Adopt
# the current DB value so the request can proceed; password rotation
# still invalidates sessions that carry an outdated non-null stamp.
if sess_stamp is None and db_stamp is not None:
session[SESSION_AUTH_STAMP_SESSION_KEY] = db_stamp
_mark_session_stamp_validated(db_stamp)
_cache_user_session_stamp(user_id, db_stamp)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: This logic accepts any authenticated session that has no stamp by copying the current DB stamp into the session and allowing the request. That means stamp-less sessions (for example older sessions or flows that missed stamp write) are not invalidated after a password change, which breaks the cross-session invalidation guarantee. Treat missing session stamp as invalid once a DB stamp exists, rather than auto-adopting it. [security]

Severity Level: Major ⚠️
❌ Stamp-less sessions remain valid after password rotation.
⚠️ Cross-device invalidation weaker than documented guarantee.
⚠️ Older pre-feature sessions not forced to reauthenticate.
Steps of Reproduction ✅
1. A user changes their AUTH_DB password via `PUT /api/v1/me/password`, which hits
`CurrentUserRestApi.update_my_password` at `superset/views/users/api.py:323-379`. This
view verifies the current password, hashes the new password, and calls
`bump_user_session_auth_stamp(g.user.id)` (imported at `superset/views/users/api.py:48`
and implemented in `superset/utils/auth_session_stamp.py:183-204`) to rotate the per-user
DB stamp.

2. The user has another authenticated browser session whose signed cookie lacks
`_auth_session_stamp` (for example, a session created before this feature or one that
missed stamp persistence). This scenario is mirrored in
`test_validate_stamp_adopts_missing_session_stamp` at
`superset/tests/unit_tests/utils/test_auth_session_stamp.py:235-257`, where `session` has
no `_auth_session_stamp` but the DB row’s `stamp` is set to `"db-stamp"`.

3. On the next request from that stamp-less session (e.g., `GET /api/v1/me/` as in the
tests), `validate_session_auth_stamp_for_request()` at
`superset/utils/auth_session_stamp.py:310-392` runs via the `before_request` hook
registered in `register_session_auth_stamp_hook()` at line 118, resolving `user_id`,
loading `db_stamp` from `UserSessionAuthStamp`, and reading `sess_stamp =
session.get(SESSION_AUTH_STAMP_SESSION_KEY)`, which is `None`.

4. Because `sess_stamp is None or sess_stamp != db_stamp` evaluates true, the branch at
lines 368-377 executes. Since `sess_stamp is None and db_stamp is not None`, the code
copies `db_stamp` into the session (`SESSION_AUTH_STAMP_SESSION_KEY`), marks the stamp as
validated, caches it, and returns without calling `logout_user()` or raising
`Unauthorized`. As a result, this stamp-less pre-rotation session remains authenticated
even after the password change, so some previously authenticated sessions are not
invalidated despite the rotated DB stamp.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_session_stamp.py
**Line:** 368:377
**Comment:**
	*Security: This logic accepts any authenticated session that has no stamp by copying the current DB stamp into the session and allowing the request. That means stamp-less sessions (for example older sessions or flows that missed stamp write) are not invalidated after a password change, which breaks the cross-session invalidation guarantee. Treat missing session stamp as invalid once a DB stamp exists, rather than auto-adopting it.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

…management

- Updated the down_revision in the migration script to reflect the latest changes.
- Improved the password reset logic in SupersetSecurityManager to rotate the session auth stamp for users when their password is reset, ensuring better security.
- Added hooks to manage per-user session stamps during login and password changes, preventing sensitive information from being logged.
- Enhanced unit tests to verify that password changes do not log sensitive data and that session stamps are correctly managed during resets.
- Reformatted conditional statement for better clarity.
- Cleaned up assertions in user API tests for improved readability.
@bito-code-review

bito-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #370093

Actionable Suggestions - 0
Review Details
  • Files reviewed - 6 · Commit Range: c547ad1..5870e62
    • superset/migrations/versions/2026-05-13_12-00_c6219cac9270_add_user_session_auth_stamp_table.py
    • superset/security/manager.py
    • superset/utils/auth_session_stamp.py
    • superset/views/users/api.py
    • tests/integration_tests/users/api_tests.py
    • tests/unit_tests/security/manager_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

Comment on lines +35 to +37
SESSION_AUTH_STAMP_SESSION_KEY = "_auth_session_stamp"
SESSION_AUTH_STAMP_VALIDATED_AT_KEY = "_auth_session_stamp_validated_at"
SESSION_AUTH_STAMP_VALIDATED_DB_STAMP_KEY = "_auth_session_stamp_validated_db_stamp"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add explicit type annotations for the new session-stamp key constants to satisfy the type-hint requirement for relevant module-level variables. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

These newly added module-level constants are plain assignments without type annotations. The custom rule requires type hints for new Python variables that can be annotated, so this is a real violation.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_session_stamp.py
**Line:** 35:37
**Comment:**
	*Custom Rule: Add explicit type annotations for the new session-stamp key constants to satisfy the type-hint requirement for relevant module-level variables.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +39 to +41
_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
_STAMP_CACHE_KEY_PREFIX = "auth_session_stamp:"
_DEFAULT_STAMP_CACHE_TIMEOUT_SECONDS = 300

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add explicit type annotations for these cache and method constants so all relevant new variables are typed. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

These are also newly introduced module-level constants without explicit type hints. Since they are relevant variables that can be annotated, they violate the type-hint requirement.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_session_stamp.py
**Line:** 39:41
**Comment:**
	*Custom Rule: Add explicit type annotations for these cache and method constants so all relevant new variables are typed.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +56 to +69
resolved: str = algorithm or get_auth_db_password_hash_algorithm()
if resolved == "argon2":
return _argon2_hasher.hash(password)
if resolved == "bcrypt":
try:
encoded: bytes = password.encode("utf-8")
except UnicodeEncodeError as exc:
raise ValueError("Password cannot be encoded as UTF-8.") from exc
if len(encoded) > BCRYPT_MAX_PASSWORD_BYTES:
raise ValueError(
f"Password exceeds bcrypt's {BCRYPT_MAX_PASSWORD_BYTES}-byte limit."
)
return bcrypt.hashpw(encoded, bcrypt.gensalt()).decode("utf-8")
raise ValueError(f"Unsupported AUTH_DB hash algorithm: {resolved}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The explicit algorithm argument is used verbatim, unlike config-derived values which are normalized in get_auth_db_password_hash_algorithm; passing values like "BCRYPT" or " argon2 " will incorrectly raise ValueError at runtime. Normalize algorithm with strip().lower() before comparison so direct callers get the same contract as config-based resolution. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Direct hash_auth_db_password callers passing uppercase algorithm crash.
- ⚠️ Inconsistent behavior versus AUTH_DB_CONFIG normalization causes confusion.
- ⚠️ Future password utilities relying on explicit algorithms become fragile.
Steps of Reproduction ✅
1. Locate the hash helper `hash_auth_db_password` in
`superset/utils/auth_db_password_hash.py:50-69`, which accepts an optional `algorithm`
parameter and sets `resolved: str = algorithm or get_auth_db_password_hash_algorithm()`.

2. Inspect the configuration resolver `get_auth_db_password_hash_algorithm` in
`superset/utils/auth_db_password.py:15-37`, which normalizes config values with `algorithm
= str(raw_algorithm).strip().lower()` before validating against
`_SUPPORTED_HASH_ALGORITHMS`.

3. Note that `hash_auth_db_password` does not normalize its explicit `algorithm` argument;
it only compares `resolved` directly to `"argon2"` and `"bcrypt"` (lines 57-59 in
`auth_db_password_hash.py`).

4. In any calling code (for example a new helper or script) invoke
`hash_auth_db_password("secret", "BCRYPT")` or `hash_auth_db_password("secret", " argon2
")`; since `resolved` becomes `"BCRYPT"` / `" argon2 "`, neither `if resolved == "argon2"`
nor `if resolved == "bcrypt"` matches, and the function raises `ValueError("Unsupported
AUTH_DB hash algorithm: BCRYPT")` at line 69 instead of hashing successfully, unlike the
normalized config-driven path.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_db_password_hash.py
**Line:** 56:69
**Comment:**
	*Api Mismatch: The explicit `algorithm` argument is used verbatim, unlike config-derived values which are normalized in `get_auth_db_password_hash_algorithm`; passing values like `"BCRYPT"` or `" argon2 "` will incorrectly raise `ValueError` at runtime. Normalize `algorithm` with `strip().lower()` before comparison so direct callers get the same contract as config-based resolution.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

bito-code-review Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #f84088

Actionable Suggestions - 0
Review Details
  • Files reviewed - 1 · Commit Range: 5870e62..837b81a
    • superset/migrations/versions/2026-05-13_12-00_c6219cac9270_add_user_session_auth_stamp_table.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

Comment on lines +44 to +50
const StrengthWrapper = styled.div`
${({ theme }) => css`
display: flex;
align-items: center;
gap: ${theme.sizeUnit * 2}px;
`}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would make sense to use the <Flex /> component here?

Comment on lines +57 to +61
.ant-progress {
margin-bottom: 0;
}
`}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we have another soulution instead of using the css class?

`;

const StrengthBarContainer = styled.div`
${({ theme }) => css`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need theme here?

Comment on lines +79 to +87
const policy = {
...AUTH_DB_DEFAULT_PASSWORD_POLICY,
password_hash_algorithm: 'argon2' as const,
password_require_uppercase: false,
password_require_lowercase: false,
password_require_digit: false,
password_require_special: false,
password_common_list_check: false,
password_min_length: 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it make sense to centralize this instead of repeating it in multiple places?

@msyavuz

msyavuz commented Jul 14, 2026

Copy link
Copy Markdown
Member

login_rate_limit, login_max_failures, and login_lockout_duration_minutes are shipped in AUTH_DB_DEFAULTS and documented, but the actual login path (auth_user_db) applies no rate limiting or lockout. The only consumer of get_auth_db_login_rate_limit_string() is the password-change endpoint (_rate_limit_me_password_change).

… Flex component for improved layout; update password policy tests to use a new minimal policy configuration
"""
Return non-secret AUTH_DB password policy options for frontend validation UI.
"""
merged_cfg = cfg if cfg is not None else get_merged_auth_db_config()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add an explicit type annotation for this local config variable so it complies with the required type-hint coverage. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This is new Python code in the added file, and the local variable merged_cfg is assigned a config mapping without an explicit type annotation even though it can be annotated. That matches the type-hint requirement.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_db_password.py
**Line:** 137:137
**Comment:**
	*Custom Rule: Add an explicit type annotation for this local config variable so it complies with the required type-hint coverage.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

if (raw && typeof raw === 'object') {
return (Object.values(raw).flat() as string[]).join(' ');
}
return t('Something went wrong while saving the user info');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: This fallback error message is shown for both profile edit and password-change submissions, but it explicitly says “saving the user info,” which is incorrect for password reset failures and will mislead users. Use an operation-appropriate fallback message for the password flow. [comment mismatch]

Severity Level: Minor 🧹
⚠️ Password reset errors misreported as user info issues.
⚠️ Can confuse users diagnosing password change failures.
Steps of Reproduction ✅
1. Inspect the shared error-message helper in
`superset-frontend/src/features/userInfo/UserInfoModal.tsx:81-93`.
`getSubmitErrorMessage()` falls back at line 92 to `t('Something went wrong while saving
the user info')` when neither `clientError.error` nor `clientError.message` provides a
usable string.

2. Examine the form submission handler in the same file at `handleFormSubmit()` on lines
104-137. For edit mode (lines 106-112), it PUTs `/api/v1/me/` to update profile info; for
password reset (lines 113-133), it PUTs `/api/v1/me/password` to change the password and
re-authenticates the session.

3. Note the shared catch block on lines 134-137: `catch (error) { addDangerToast(await
getSubmitErrorMessage(error)); throw error; }`. This is used for both the profile edit
flow and the password-change flow, meaning the fallback string from step 1 is shown on any
generic failure regardless of which endpoint failed.

4. Trigger a generic backend or network error on the password-change endpoint (e.g., cause
a 500 or a timeout so that `getClientErrorObject()` yields no specific message). The
password reset submission from the ChangePasswordModal (wired in
`superset-frontend/src/pages/UserInfo/index.tsx:245-253`) results in a toast with the
fallback `Something went wrong while saving the user info`, which inaccurately describes a
password-change failure as a user-info save issue and can mislead users diagnosing
password problems.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/features/userInfo/UserInfoModal.tsx
**Line:** 92:92
**Comment:**
	*Comment Mismatch: This fallback error message is shown for both profile edit and password-change submissions, but it explicitly says “saving the user info,” which is incorrect for password reset failures and will mislead users. Use an operation-appropriate fallback message for the password flow.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +201 to +212
const pwd = generateAuthDbPassword(passwordPolicy);
form.setFieldsValue({
new_password: pwd,
confirm_password: pwd,
});
// setFieldsValue does not fire the form's change handlers, so
// validate the affected fields to recompute submit state.
form
.validateFields(['new_password', 'confirm_password'])
.catch(() => {});
}}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The password generator call can throw (for example when server policy is impossible to satisfy, such as bcrypt with a minimum length above 72 bytes), but this click handler does not catch that exception. That will surface as an uncaught runtime error and break the modal instead of showing a user-facing validation/toast message. Catch generator failures in this handler and report a friendly error without crashing the form. [possible bug]

Severity Level: Major ⚠️
❌ Password generator click can crash reset password modal.
⚠️ Users lose feedback when policy or crypto misconfigured.
Steps of Reproduction ✅
1. Open the password generator UI wiring in
`superset-frontend/src/features/userInfo/UserInfoModal.tsx:176-215`. On lines 198-213, the
`Input.Password` suffix renders `GeneratePasswordInputSuffix` with `onGenerate={() => {
const pwd = generateAuthDbPassword(passwordPolicy); ... }}` (lines 201-212).

2. Inspect the generator implementation at
`superset-frontend/src/utils/generateAuthDbPassword.ts:286-310`.
`generateAuthDbPassword()` (lines 290-309) calls helpers that can throw:

   - `secureRandomInt()` (lines 139-148) throws if `globalThis.crypto.getRandomValues` is
   unavailable.

   - The generator loop throws `new Error('generateAuthDbPassword: exhausted retries')` at
   line 309 if no password can satisfy the supplied `AuthDbPasswordPolicy` (e.g., an
   impossible combination of minimum length and bcrypt byte limit).

3. Follow the user-facing path: the UserInfo page at
`superset-frontend/src/pages/UserInfo/index.tsx:91-269` opens `ChangePasswordModal` (lines
245-253), which renders `UserInfoModal` with `isEditMode={false}`. This causes
`ResetPasswordFields` (lines 162-260) to be shown, including the generator button wired to
the `onGenerate` handler above.

4. In an environment without Web Crypto (`globalThis.crypto.getRandomValues` missing) or
under a misconfigured password policy that cannot be satisfied, clicking the "Generate
password" button (rendered by `GeneratePasswordInputSuffix` in
`superset-frontend/src/components/GeneratePasswordInputSuffix.tsx:29-48`) will propagate
the exception from `generateAuthDbPassword()` through `onGenerate` without any try/catch.
React treats this as an uncaught error during the click event, which can break the modal
or hit a global error boundary instead of displaying a toast or validation error,
confirming the lack of defensive handling on this path.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/features/userInfo/UserInfoModal.tsx
**Line:** 201:212
**Comment:**
	*Possible Bug: The password generator call can throw (for example when server policy is impossible to satisfy, such as bcrypt with a minimum length above 72 bytes), but this click handler does not catch that exception. That will surface as an uncaught runtime error and break the modal instead of showing a user-facing validation/toast message. Catch generator failures in this handler and report a friendly error without crashing the form.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

do_upgrade(session)
try:
session.commit()
session.commit() # pylint: disable=consider-using-transaction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Calling session.commit() inside an Alembic migration breaks the outer Alembic-managed transaction boundary. If a failure happens after this commit (for example while stamping migration state), the permission changes can be permanently committed while the revision is not fully recorded, leaving migration state inconsistent on reruns. Keep this work inside Alembic’s transaction and flush the session instead of committing here. [logic error]

Severity Level: Critical 🚨
- ❌ Migration data committed while Alembic version not recorded.
- ⚠️ Rerunning migrations sees inconsistent permissions versus revision.
Steps of Reproduction ✅
1. From a Superset instance using this PR code, run `superset db upgrade` so Alembic
executes migrations via `superset/migrations/env.py:64-84`, where
`context.run_migrations()` is wrapped inside `context.begin_transaction()`.

2. During the run, Alembic loads revision module
`superset/migrations/versions/2026-06-23_03-23_a7d3f1b9c2e4_cleanup_stale_can_import_pvm.py`
and calls its `upgrade()` function defined around line 99 in the PR hunk.

3. Inside `upgrade()`, `Session(bind=op.get_bind())` is created, `do_upgrade(session)`
mutates permission rows, and `session.commit()` at line 104 commits those changes directly
on the Alembic-managed connection, independently of Alembic’s outer transaction in
`env.py:82`.

4. Introduce a later migration that intentionally raises an exception after this revision
(for example, a failing DDL in another script), rerun `superset db upgrade`, and observe
that Alembic aborts and may not fully record the revision chain while the permission
updates committed by `upgrade()` remain persisted, demonstrating desynchronized data
versus migration version tracking.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/migrations/versions/2026-06-23_03-23_a7d3f1b9c2e4_cleanup_stale_can_import_pvm.py
**Line:** 104:104
**Comment:**
	*Logic Error: Calling `session.commit()` inside an Alembic migration breaks the outer Alembic-managed transaction boundary. If a failure happens after this commit (for example while stamping migration state), the permission changes can be permanently committed while the revision is not fully recorded, leaving migration state inconsistent on reruns. Keep this work inside Alembic’s transaction and flush the session instead of committing here.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread superset/config.py
# How often (seconds) to re-check the session auth stamp against the database on
# read-only requests (GET/HEAD/OPTIONS). State-changing requests always
# revalidate. Set to 0 to check on every authenticated request.
SESSION_AUTH_STAMP_REVALIDATION_SECONDS: int = 300

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Setting the default revalidation interval to 300 seconds introduces a security window where stale sessions can keep passing GET/HEAD/OPTIONS checks without a DB lookup. This is especially problematic for password changes performed outside the self-service flow (for example admin resets), where the stamp is bumped but the shared stamp cache is not immediately refreshed, allowing old sessions to continue reading data until the interval expires. Use a default of 0 (always revalidate) or ensure every stamp bump path updates the shared cache immediately. [security]

Severity Level: Critical 🚨
❌ Admin ResetPasswordView leaves compromised sessions valid during TTL.
❌ GET /api/v1/me/ still authorized after admin reset.
⚠️ Cross-node session invalidation delayed for AUTH_DB admin resets.
Steps of Reproduction ✅
1. Start Superset with AUTH_DB enabled and the default configuration in
`superset/config.py` lines 465–476, where `AUTH_TYPE = AUTH_DB` and
`SESSION_AUTH_STAMP_REVALIDATION_SECONDS: int = 300` is set.

2. Log in as an AUTH_DB user to create a session stamp; on successful login,
`SupersetSecurityManager.on_user_login` in `superset/security/manager.py:1232–1241` calls
`sync_session_auth_stamp_on_login(user)` from
`superset/utils/auth_session_stamp.py:153–165`, which writes the stamp into the signed
session cookie, marks it validated via `_mark_session_stamp_validated` at
`auth_session_stamp.py:261–265`, and caches it with `_cache_user_session_stamp`.

3. As an administrator, reset this same user’s password via the FAB “Reset Password” flow
(ResetPasswordView), which routes through `SupersetSecurityManager.reset_password` in
`superset/security/manager.py:1161–1218`; when `AUTH_TYPE == AUTH_DB`, this method calls
`bump_user_session_auth_stamp(target_user_id)` at `auth_session_stamp.py:172–194` to
rotate the DB-stored stamp but does not call `cache_user_session_auth_stamp`
(`auth_session_stamp.py:167–170`), leaving the shared cache and existing browser sessions
with the old stamp.

4. Within 300 seconds of the last stamp validation, have the affected user reuse their
existing browser session to issue a read-only request such as `GET /api/v1/me/`, handled
by `CurrentUserRestApi.get_me` in `superset/views/users/api.py:192–217`; the
`before_request` hook `_validate_user_session_auth_stamp` in
`auth_session_stamp.py:108–111` invokes `validate_session_auth_stamp_for_request`
(`auth_session_stamp.py:300–382`), which consults `_should_skip_stamp_db_lookup` at
`auth_session_stamp.py:232–258`. Because `SESSION_AUTH_STAMP_REVALIDATION_SECONDS` is 300,
the session’s `SESSION_AUTH_STAMP_VALIDATED_AT_KEY` timestamp is recent, and the cached
stamp still matches the old session stamp (no cache bump happened in `reset_password`),
`_should_skip_stamp_db_lookup` returns `True` and the DB lookup is skipped, allowing the
stale session to continue reading data until the revalidation interval and cache TTL
expire.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/config.py
**Line:** 476:476
**Comment:**
	*Security: Setting the default revalidation interval to 300 seconds introduces a security window where stale sessions can keep passing GET/HEAD/OPTIONS checks without a DB lookup. This is especially problematic for password changes performed outside the self-service flow (for example admin resets), where the stamp is bumped but the shared stamp cache is not immediately refreshed, allowing old sessions to continue reading data until the interval expires. Use a default of 0 (always revalidate) or ensure every stamp bump path updates the shared cache immediately.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +4615 to +4621
current_app.config.get(
"AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
DEFAULT_AUTH_DB_FAKE_PASSWORD_HASH_CHECK,
),
"password",
)
logger.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The failed-login timing equalization path ignores the attacker-supplied password and always checks the literal string "password" against a Werkzeug hash. Because real-user checks use verify_auth_db_password(user.password, password) (bcrypt/argon2/legacy) while missing-user checks use a different verifier and fixed input, login timing can diverge enough to leak account existence. Use the same candidate password and the same verification path for both branches (or a fake hash compatible with the same verifier) to avoid user-enumeration side channels. [security]

Severity Level: Critical 🚨
- ❌ AUTH_DB login leaks account existence via timing variance.
- ⚠️ Enables username enumeration with crafted long passwords.
Steps of Reproduction ✅
1. Inspect AUTH_DB login helper `auth_user_db()` in
`superset/security/manager.py:4599-131` (loaded via BulkRead), which authenticates
database users when `AUTH_TYPE` = `AUTH_DB`.

2. For a missing or inactive user, follow the branch at `manager.py:113-125`: the code
calls `check_password_hash(current_app.config.get("AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
DEFAULT_AUTH_DB_FAKE_PASSWORD_HASH_CHECK), "password")`, using the literal `"password"`
instead of the attacker-supplied candidate.

3. For an existing active user, follow the branch at `manager.py:126-131`: it calls
`verify_auth_db_password(user.password, password)` with the attacker-supplied `password`.
In `superset/utils/auth_db_password_hash.py:72-103`, the bcrypt path (`lines 81-88`)
encodes the candidate and short-circuits to `False` when `len(encoded) >
BCRYPT_MAX_PASSWORD_BYTES`.

4. Configure AUTH_DB to use bcrypt (default in
`superset/utils/auth_db_password.py:29-42`), create a real user with a bcrypt hash, and
compare login timings for that username versus a non-existent username using an overlong
password (>72 UTF-8 bytes): the real-user path for bcrypt short-circuits quickly, while
the missing-user path always runs the full PBKDF2-based `check_password_hash` on
`"password"`, producing measurable timing differences that can be used to infer account
existence.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/security/manager.py
**Line:** 4615:4621
**Comment:**
	*Security: The failed-login timing equalization path ignores the attacker-supplied password and always checks the literal string `"password"` against a Werkzeug hash. Because real-user checks use `verify_auth_db_password(user.password, password)` (bcrypt/argon2/legacy) while missing-user checks use a different verifier and fixed input, login timing can diverge enough to leak account existence. Use the same candidate password and the same verification path for both branches (or a fake hash compatible with the same verifier) to avoid user-enumeration side channels.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +37 to +41
"reset_token_expiry_minutes": 30,
"reset_rate_limit": "5 per 15 minutes",
"login_rate_limit": "10 per 5 minutes",
"login_max_failures": 5,
"login_lockout_duration_minutes": 15,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: These lockout/reset policy keys are introduced as configurable behavior but are not consumed anywhere in the backend auth flow, so changing them has no effect at runtime. This creates a misleading security contract for operators who expect lockout and reset controls to be enforced. Either wire these settings into the corresponding authentication/reset logic or remove them until enforcement is implemented. [incomplete implementation]

Severity Level: Critical 🚨
- ⚠️ Operators misled; lockout settings silently ineffective.
- ⚠️ Increased risk of password-guessing without configured lockout.
Steps of Reproduction ✅
1. Inspect `AUTH_DB_DEFAULTS` in `superset/utils/auth_db_password.py:29-42` (BulkRead
output): it introduces `reset_token_expiry_minutes`, `reset_rate_limit`,
`login_rate_limit`, `login_max_failures`, and `login_lockout_duration_minutes` as
configurable AUTH_DB policy options.

2. Use Grep across `/workspace/superset` (tool results included) for each key:
`login_max_failures`, `reset_rate_limit`, `reset_token_expiry_minutes`, and
`login_lockout_duration_minutes` only appear in `auth_db_password.py:29-42` and are never
referenced elsewhere; `login_rate_limit` is used via
`get_auth_db_login_rate_limit_string()` in `superset/views/users/api.py:40-46,88-91` to
rate-limit `PUT /api/v1/me/password`, confirming the other knobs are truly unused.

3. Inspect the AUTH_DB login flow in `superset/security/manager.py:4599-131`:
`auth_user_db()` calls `find_user()` and `verify_auth_db_password()` to decide success and
uses `update_user_auth_stat()` on failure, but no code reads `login_max_failures` or
`login_lockout_duration_minutes`, so failed-login counts and lockout duration are not
enforced.

4. Inspect the password change API in `superset/views/users/api.py:340-64`: it loads the
request body, verifies the current password via `verify_auth_db_password()`, validates and
hashes the new password, and commits the change, but never consults
`reset_token_expiry_minutes` or `reset_rate_limit`. As a result, operators can set these
AUTH_DB_CONFIG values expecting lockout or reset semantics, yet the backend
authentication/reset flows ignore them and system behavior does not change.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_db_password.py
**Line:** 37:41
**Comment:**
	*Incomplete Implementation: These lockout/reset policy keys are introduced as configurable behavior but are not consumed anywhere in the backend auth flow, so changing them has no effect at runtime. This creates a misleading security contract for operators who expect lockout and reset controls to be enforced. Either wire these settings into the corresponding authentication/reset logic or remove them until enforcement is implemented.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@EnxDev

EnxDev commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

EnxDev's Review Agent — #39469 · HEAD 2ec20b8

request changes — Pillar 1 is now substantively delivered and the split was honored, but the new migration forks the Alembic tree (again) and CI is red at this HEAD.

Supersedes my review at f0de405. Both blockers raised there are addressed in spirit: the plaintext-password logging is fixed (manual _log_audit_event with an id-only payload, guarded by test_put_my_password_does_not_log_passwords on success and failure paths), and the migration was re-pointed — but at a revision that has since gained another child on master, so the tree forks again.

Pillar 1 status vs. the SIP (your May 3 checklist)

SIP Item Status
Multi-session invalidation user_session_auth_stamp + before_request check implemented. Integration test verifies that a cloned session cookie receives 401 (one gap remains; see findings below).
Remember-me token revocation ⚠️ Cookie is cleared only on the browser where the password is changed. See the stamp-adoption finding below.
Password confirmation field ✅ Implemented with validates_schema, UI field, and integration test.
Real-time strength / policy indicator ✅ Implemented via GET /me/password/policy and AuthDbPasswordPolicyIndicator.
Audit metadata (self/admin, source IP) ❌ Not implemented (see finding below).
password_hash_algorithm (bcrypt/argon2) ✅ Implemented, including legacy hash fallback, bcrypt 72-byte handling, and test coverage.

Split status: ✅ admin-reset UI/API and auth_audit_log are out of the diff (existing Log reused per dpgaspar). Prior blockers verified fixed at this HEAD: doubled UserLoggedIn audits, cache-before-commit race, session-clear-before-commit, CSRF rotation via SupersetClient.reAuthenticate().

🔴 Functional

  • superset/migrations/versions/2026-05-13_12-00_c6219cac9270_add_user_session_auth_stamp_table.py:31 · High — down_revision = "3a8e6f2c1b95" collides with master: b1c2d3e4f5a6 (add_subjects_tables) already claims that same parent, so the merged tree has two heads and superset db upgrade aborts — today's test-load-examples failure is exactly Error: Multiple head revisions are present… at superset init. Third round of this failure mode (b4a3f2e1d0c9 → 9e1f3b8c4d2a → 3a8e6f2c1b95): hand-picking a revision goes stale as master moves. Rebase onto current master and set down_revision from superset db heads at rebase time, in the same push. (Note: the "Check DB migration conflict" workflow passing is meaningless — it only posts courtesy comments, it never runs alembic heads.)

🟡 Should-fix

  • superset/views/users/api.py:400 + superset/security/manager.py:1339 — the audit-metadata SIP item is still open. UserPasswordChanged carries only {"user_id"} — no self/admin flag, no source IP (the Log row has no IP column and the payload doesn't include one). Worse, the admin path (reset_password, still reachable via FAB's Reset Password action) emits no password-change audit event at all. Since Log is being reused, put it in the JSON payload — {"user_id", "initiated_by": "self"|"admin", "actor_id", "source_ip"} — and emit the same event from the reset_password admin branch.
  • superset/utils/auth_session_stamp.py:363 — a session with no stamp in the cookie adopts the current DB stamp instead of being invalidated. That covers pre-upgrade sessions (defensible rollout window) but also sessions re-established from a Flask-Login remember cookie after a password change — remember processing sets _user_id without running on_user_login, so a stolen remember token survives rotation in deployments that enable remember-me (clear_flask_login_remember_cookie:139 only clears the changing browser's cookie). Suggest adopting only when session.get("_fresh") is truthy, or documenting the limitation.
  • superset/security/manager.py — pre-commit is red: ruff I001, the added LOGMSG_WAR_SEC_LOGIN_FAILED import is unsorted. One-line fix; include in the rebase push.

🔵 Nits

  • superset/views/users/api.py:292 — "password" in request.json raises TypeError (→ 500) when the JSON body is a scalar (5, true); guard with isinstance(request.json, dict).
  • superset/security/manager.py:4862 — the timing-balance fake check runs werkzeug pbkdf2 while real users verify bcrypt/argon2, so unknown-username failures are measurably faster — a username-enumeration timing oracle. Hash the dummy with the configured algorithm.

🙌 Praise

  • superset/views/users/api.py:369 — the optimistic lock (filter(User.password == old_hash)) with post-commit-only cache write closes both prior races cleanly, and the conflict/500 paths now have tests.

@bito-code-review

bito-code-review Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #187689

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/migrations/versions/2026-05-13_12-00_c6219cac9270_add_user_session_auth_stamp_table.py - 1
    • Migration dependency chronological anomaly · Line 20-20
      The `down_revision` was changed from `3a8e6f2c1b95` to `d24e6b0a9c7f`. Both revisions exist and the chain `c6219cac9270` → `d24e6b0a9c7f` → `8f3a1b2c4d5e` is internally consistent. However, note that `d24e6b0a9c7f` has Create Date 2026-06-30, which is one month after this migration's Create Date (2026-05-13). In Alembic, forward-referencing a later migration is valid but unusual — confirm this is intentional ordering rather than an accidental dependency. ([Alembic migration dependency ordering](https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script))
Review Details
  • Files reviewed - 4 · Commit Range: 837b81a..7803851
    • superset-frontend/src/components/AuthDbPasswordPolicyIndicator.tsx
    • superset-frontend/src/utils/generateAuthDbPassword.test.ts
    • superset/migrations/versions/2026-05-13_12-00_c6219cac9270_add_user_session_auth_stamp_table.py
    • superset/security/manager.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
    • Eslint (Linter) - ✔︎ 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

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shantanukhond I think we're in a really good place with this overall; thanks for driving this Pillar 1 forward!

Let me leave just couple of points that I think are worth addressing before we merge.
One thing I noticed is that the source IP mentioned in SIP-201 still seems to be missing from the audit metadata.
What do you think about that here?

Comment on lines 1339 to +1395
@@ -1334,22 +1349,50 @@ def reset_password(self, userid: Union[int, str], password: str) -> None:
bypassed. We distinguish the two by comparing the acting user
(``g.user``) against the target ``userid``: they match for a
self-service reset and differ for an admin reset.

On ``AUTH_DB``, the per-user session auth stamp is rotated so every
other session for the target account is invalidated — including when an
administrator resets a compromised password.
"""
# pylint: disable=import-outside-toplevel
from flask_appbuilder.const import AUTH_DB

super().reset_password(userid, password)

try:
target_user_id = int(userid)
except (TypeError, ValueError):
target_user_id = None

acting_user = getattr(g, "user", None)
acting_user_id = getattr(acting_user, "id", None)
# ``userid`` arrives as a string (the ``pk`` request arg) on the admin
# path, so coerce both sides before comparing.
is_self_service = acting_user_id is not None and self._same_user(
acting_user_id, userid
is_self_service = (
acting_user_id is not None
and target_user_id is not None
and self._same_user(acting_user_id, userid)
)
if is_self_service:

if (
current_app.config.get("AUTH_TYPE") == AUTH_DB
and target_user_id is not None
):
from superset.utils.auth_session_stamp import (
bump_user_session_auth_stamp,
sync_session_auth_stamp_on_login,
)

bump_user_session_auth_stamp(target_user_id)
if is_self_service and acting_user is not None:
sync_session_auth_stamp_on_login(acting_user)

if is_self_service and target_user_id is not None:
from superset.security.password_change import (
clear_password_must_change,
)

clear_password_must_change(int(userid))
clear_password_must_change(target_user_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shantanukhond would it make sense to add an _log_audit_event here so we can track who changed the password?

Comment thread superset/views/users/api.py Outdated

cache_user_session_auth_stamp(g.user.id, new_stamp)
_reestablish_login_session(user_after)
_log_audit_event("UserPasswordChanged", {"user_id": g.user.id})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it make sense to add more details here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding initiated_by and source_ip. Anything else you can suggest?

Comment thread superset/utils/auth_session_stamp.py Outdated
Comment on lines +363 to +364
if sess_stamp is None and db_stamp is not None:
session[SESSION_AUTH_STAMP_SESSION_KEY] = db_stamp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had one thought about the stamp adoption logic. It looks like when a session is restored from a remember-me cookie, there's no stamp in the session, so the current code adopts the latest DB stamp and the session remains valid.
Would it make sense to only adopt the stamp for fresh (session["_fresh"]) interactive logins, and instead reject non-fresh restored sessions (logout + clear the remember cookie + 401)?

raw_algorithm: Any = merged_cfg.get(
"password_hash_algorithm", AUTH_DB_DEFAULTS["password_hash_algorithm"]
)
algorithm = str(raw_algorithm).strip().lower()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add an explicit type hint for this derived local value to satisfy the rule requiring type hints on relevant variables. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This new local variable is a derived string value that can be explicitly annotated, but the code omits a type hint. That is a real instance of the type-hint rule violation.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_db_password.py
**Line:** 155:155
**Comment:**
	*Custom Rule: Add an explicit type hint for this derived local value to satisfy the rule requiring type hints on relevant variables.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

do_downgrade(session)
try:
session.commit()
session.commit() # pylint: disable=consider-using-transaction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The same explicit session.commit() in downgrade() has the same transaction-boundary problem and can desynchronize downgrade behavior from Alembic’s transaction handling; this should also remain under Alembic-managed transaction control with flush-only ORM persistence. [logic error]

Severity Level: Critical 🚨
- ❌ Downgrade transaction boundaries inconsistent with Alembic env.py control.
- ⚠️ Rollback of failed downgrade cannot fully revert permission changes.
Steps of Reproduction ✅
1. Run database downgrades via Superset (for example `superset db downgrade`), which uses
Alembic’s env runner at `superset/migrations/env.py:87-142` to call
`run_migrations_online()` and, for MySQL/SQLite, sets `transaction_per_migration=True` and
`transactional_ddl=True` (line 118-119) while wrapping `context.run_migrations()` inside
`context.begin_transaction()` (lines 131-133).

2. During this migration run, Alembic executes revision `a7d3f1b9c2e4`’s `downgrade()`
function defined at
`superset/migrations/versions/2026-06-23_03-23_a7d3f1b9c2e4_cleanup_stale_can_import_pvm.py:110-120`,
which creates `session = Session(bind=bind)` (line 112) and calls `do_downgrade(session)`
(line 113), currently a documented no-op (lines 86-96).

3. After `do_downgrade()` returns, `downgrade()` explicitly calls `session.commit()` at
line 115, committing the database transaction from inside the revision instead of leaving
transaction control to Alembic’s `context.begin_transaction()` in `env.py`.

4. If a subsequent downgrade step in the same Alembic-managed transaction fails and
Alembic attempts to roll back, the manual commit has already finalized this revision’s
effects, so downgrade behavior becomes desynchronized from Alembic’s transactional
guarantees and rollback reliability now depends on this explicit commit rather than the
configured per-migration transaction.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/migrations/versions/2026-06-23_03-23_a7d3f1b9c2e4_cleanup_stale_can_import_pvm.py
**Line:** 115:115
**Comment:**
	*Logic Error: The same explicit `session.commit()` in `downgrade()` has the same transaction-boundary problem and can desynchronize downgrade behavior from Alembic’s transaction handling; this should also remain under Alembic-managed transaction control with flush-only ORM persistence.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +4863 to +4868
current_app.config.get(
"AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
DEFAULT_AUTH_DB_FAKE_PASSWORD_HASH_CHECK,
),
"password",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The fake password check for unknown/inactive users always uses a Werkzeug PBKDF2 hash, while real users are now verified with bcrypt/argon2 via verify_auth_db_password; this creates a measurable timing difference between “user exists” and “user does not exist” paths and reintroduces username-enumeration risk. Use a fake hash generated with the same configured AUTH_DB algorithm (or route both paths through the same verifier) so failed-login timing stays consistent. [security]

Severity Level: Major ⚠️
- ❌ AUTH_DB login exposes username existence via timing side-channel.
- ⚠️ Facilitates remote enumeration of valid AUTH_DB user accounts.
- ⚠️ Undermines protection provided by fake password hash checks.
Steps of Reproduction ✅
1. Observe how the security manager is wired into Superset:
`superset/initialization/__init__.py:1209-1218` sets `appbuilder.security_manager_class =
SupersetSecurityManager`, and `SupersetSecurityManager.register_views` in
`superset/security/manager.py:4880-4887` registers `SupersetAuthView` from
`superset/views/auth.py:33-42` as the `/login` view, so database logins ultimately
authenticate through this manager.

2. Inspect `SupersetSecurityManager.auth_user_db` in
`superset/security/manager.py:4847-4879`: when `user is None or (not user.is_active)` at
line 4861, the code executes the fake password check at lines 4863-4868, calling
`werkzeug.security.check_password_hash` with
`current_app.config["AUTH_DB_FAKE_PASSWORD_HASH_CHECK"]` (falling back to
`DEFAULT_AUTH_DB_FAKE_PASSWORD_HASH_CHECK`, a PBKDF2 hash defined at lines 125-132); when
a user exists, line 4874 instead calls `verify_auth_db_password(user.password, password)`.

3. Inspect the real verifier and password algorithm defaults: `verify_auth_db_password` in
`superset/utils/auth_db_password_hash.py:72-103` uses bcrypt (`bcrypt.checkpw`) when the
stored hash matches the bcrypt format and argon2 (`argon2.PasswordHasher.verify`) when it
starts with `"$argon2"`, only falling back to Werkzeug `check_password_hash` for legacy
hashes; `superset/utils/auth_db_password.py:28-37` and `144-166` show that
`AUTH_DB_CONFIG["password_hash_algorithm"]` defaults to `"bcrypt"`, so newly created
AUTH_DB users will typically have bcrypt hashes.

4. Run Superset with `AUTH_TYPE = AUTH_DB` and default `AUTH_DB_CONFIG`, create an active
AUTH_DB user `alice` (whose password hash will be bcrypt by default), then from an HTTP
client issue repeated login attempts: (a) POST to `/login` as `alice` with an incorrect
password, which drives the login stack to call `auth_user_db` and execute the
bcrypt/argon2 verification path via `verify_auth_db_password`; (b) POST to `/login` with a
non-existent username such as `ghost` and any password, which drives `auth_user_db` into
the `user is None` branch and executes only the PBKDF2 fake hash check at
`superset/security/manager.py:4863-4868`. Measuring average response times for case (a) vs
case (b) over many requests will reveal a timing difference attributable to the different
hash algorithms/parameters on these two failure paths, enabling remote username
enumeration despite the intended fake-check mitigation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/security/manager.py
**Line:** 4863:4868
**Comment:**
	*Security: The fake password check for unknown/inactive users always uses a Werkzeug PBKDF2 hash, while real users are now verified with bcrypt/argon2 via `verify_auth_db_password`; this creates a measurable timing difference between “user exists” and “user does not exist” paths and reintroduces username-enumeration risk. Use a fake hash generated with the same configured `AUTH_DB` algorithm (or route both paths through the same verifier) so failed-login timing stays consistent.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +124 to +128
return str(
get_merged_auth_db_config().get(
"login_rate_limit", AUTH_DB_DEFAULTS["login_rate_limit"]
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: This coerces any configured login_rate_limit value to a string, which turns invalid values like None into the literal string "None" and can break Flask-Limiter parsing at request time (causing endpoint failures instead of a controlled fallback). Validate the value and fall back to the default when it is missing or malformed instead of unconditional str(...) coercion. [logic error]

Severity Level: Major ⚠️
- ❌ Password change endpoint 500s when login_rate_limit misconfigured.
- ⚠️ Misconfigured limiter string blocks self-service password changes.
Steps of Reproduction ✅
1. Enable request rate limiting so the password-change endpoint uses Flask-Limiter by
setting `RATELIMIT_ENABLED = True` as defined in `superset/config.py:12-15`, and ensure a
`limiter` instance is attached to `security_manager` (checked in
`_rate_limit_me_password_change` at `superset/views/users/api.py:82-86`).

2. Configure an invalid AUTH_DB login rate limit by setting
`app.config["AUTH_DB_CONFIG"]["login_rate_limit"] = None` (or another non-string/invalid
object); `get_merged_auth_db_config` in `superset/utils/auth_db_password.py:104-115`
merges this value without validating the `login_rate_limit` field.

3. Issue a `PUT /api/v1/me/password` request, which is handled by
`CurrentUserRestApi.update_my_password` decorated with `@_rate_limit_me_password_change`
at `superset/views/users/api.py:310-319`; inside the wrapper,
`limiter.limit(get_auth_db_login_rate_limit_string(),
key_func=_me_password_rate_limit_key, methods=["PUT"])` is called at
`superset/views/users/api.py:28-32`.

4. `get_auth_db_login_rate_limit_string` at `superset/utils/auth_db_password.py:118-128`
coerces the invalid `login_rate_limit` value to the literal string `"None"` via
`str(...)`; when Flask-Limiter attempts to parse this string as a rate-limit expression,
it raises an exception, causing the password-change request to fail with an internal error
instead of applying a safe default limit or allowing the request.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/auth_db_password.py
**Line:** 124:128
**Comment:**
	*Logic Error: This coerces any configured `login_rate_limit` value to a string, which turns invalid values like `None` into the literal string `"None"` and can break Flask-Limiter parsing at request time (causing endpoint failures instead of a controlled fallback). Validate the value and fall back to the default when it is missing or malformed instead of unconditional `str(...)` coercion.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

- Updated the logging of password changes to include the initiator and source IP address.
- Introduced a new function to invalidate stale authentication sessions, ensuring proper session management after password changes.
- Adjusted tests to verify the new logging structure and session invalidation behavior for both self-service and admin-initiated password changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API authentication Related to authentication change:backend Requires changing the backend change:frontend Requires changing the frontend doc Namespace | Anything related to documentation risk:breaking-change Issues or PRs that will introduce breaking changes risk:ci-script PR modifies scripts that execute in CI (supply chain risk) risk:db-migration PRs that require a DB migration size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants