Skip to content

feat: role/user CRUD events and login/logout tracking in the action log#39121

Merged
dpgaspar merged 12 commits intoapache:masterfrom
preset-io:danielgaspar/sc-102915/visa-pcs-include-fab-security-events-on-superset
Apr 9, 2026
Merged

feat: role/user CRUD events and login/logout tracking in the action log#39121
dpgaspar merged 12 commits intoapache:masterfrom
preset-io:danielgaspar/sc-102915/visa-pcs-include-fab-security-events-on-superset

Conversation

@dpgaspar
Copy link
Copy Markdown
Member

@dpgaspar dpgaspar commented Apr 6, 2026

SUMMARY

Adds security audit logging for FAB (Flask-AppBuilder) security events to Superset's action log. This enables tracking of user/role/group lifecycle events through the configured EVENT_LOGGER, ensuring that custom implementations (e.g. S3EventLogger) also receive these events.

flask-appbuilder 5.2.1 log hooks implemented here: dpgaspar/Flask-AppBuilder#2450

Events logged:

  • Authentication: UserLoggedIn, UserLoginFailed, UserLoggedOut
  • User CRUD: UserCreated, UserUpdated, UserDeleted
  • Role CRUD: RoleCreated, RoleUpdated, RoleDeleted
  • Group CRUD: GroupCreated, GroupUpdated, GroupDeleted

Implementation:

  • Overrides FAB's post_add/post_update/post_delete hooks on SupersetRoleApi, SupersetUserApi, and new SupersetGroupApi
  • Overrides on_user_login, on_user_login_failed, on_user_logout hooks on SupersetSecurityManager
  • All events are delegated through the AbstractEventLogger interface (event_logger.log()), so any configured logger receives them
  • DBEventLogger uses an independent DB session to avoid expiring objects in the main scoped session (prevents DetachedInstanceError)
  • Requires flask-appbuilder>=5.2.1 which adds the auth event hooks

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A - backend-only change, no UI impact.

ADDITIONAL INFORMATION

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

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 6, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 16410af
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/69d78970bd220f00089f685e
😎 Deploy Preview https://deploy-preview-39121--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 6, 2026

Codecov Report

❌ Patch coverage is 48.97959% with 125 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.44%. Comparing base (3a3a653) to head (db88a17).
⚠️ Report is 20 commits behind head on master.

Files with missing lines Patch % Lines
superset/charts/data/dashboard_filter_context.py 29.26% 87 Missing ⚠️
superset/security/manager.py 64.70% 12 Missing ⚠️
superset/viz.py 47.05% 9 Missing ⚠️
superset/charts/data/api.py 84.61% 2 Missing and 4 partials ⚠️
superset/commands/report/execute.py 20.00% 2 Missing and 2 partials ⚠️
superset/mcp_service/sql_lab/schemas.py 69.23% 4 Missing ⚠️
superset/daos/base.py 66.66% 1 Missing ⚠️
superset/models/helpers.py 0.00% 1 Missing ⚠️
superset/utils/webdriver.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #39121      +/-   ##
==========================================
- Coverage   64.47%   64.44%   -0.03%     
==========================================
  Files        2541     2542       +1     
  Lines      131674   131834     +160     
  Branches    30524    30528       +4     
==========================================
+ Hits        84893    84965      +72     
- Misses      45315    45384      +69     
- Partials     1466     1485      +19     
Flag Coverage Δ
hive 40.05% <34.69%> (-0.03%) ⬇️
mysql 60.75% <48.97%> (+<0.01%) ⬆️
postgres 60.83% <48.97%> (+<0.01%) ⬆️
presto 41.85% <47.34%> (+1.75%) ⬆️
python 62.42% <48.97%> (+0.01%) ⬆️
sqlite 60.46% <48.97%> (+<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 Sentry.
📢 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.

dpgaspar and others added 6 commits April 8, 2026 15:30
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delegate audit events directly to the configured event_logger
(AbstractEventLogger) instead of maintaining a separate
_log_audit_event_to_db helper with its own independent session.
This ensures all logger implementations (DBEventLogger, S3EventLogger,
etc.) receive security audit events through the same interface.
The independent session handling for DB logging already exists in
DBEventLogger.log().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…include-fab-security-events-on-superset' into danielgaspar/sc-102915/visa-pcs-include-fab-security-events-on-superset
@dpgaspar dpgaspar marked this pull request as ready for review April 9, 2026 11:44
@dosubot dosubot bot added authentication Related to authentication logging Creates a UI or API endpoint that could benefit from logging. labels Apr 9, 2026
FAB 5.2.1 fixes the DetachedInstanceError that occurred when
DBEventLogger.log committed via db.session, expiring the User
object and breaking subsequent FAB logout/auth operations.
The independent session workaround is no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +228 to +234
user = MagicMock(spec=User)
user.username = "testuser"
user.id = 7

sm.on_user_login_failed(user)

mock_log.assert_called_once_with(
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 hook in FAB is triggered with the attempted username (not an authenticated user object), so testing it with a User mock validates the wrong contract and can miss production failures. Update the test to pass a username string and assert the corresponding payload. [type error]

Severity Level: Critical 🚨
- ❌ Failed logins crash due to AttributeError in SupersetSecurityManager.
- ❌ Audit logger never records failed login attempts.
- ⚠️ Unit test misrepresents Flask-AppBuilder login-failure hook contract.
Suggested change
user = MagicMock(spec=User)
user.username = "testuser"
user.id = 7
sm.on_user_login_failed(user)
mock_log.assert_called_once_with(
username = "testuser"
sm.on_user_login_failed(username)
mock_log.assert_called_once_with(
"UserLoginFailed", {"username": "testuser"}
Steps of Reproduction ✅
1. In `superset/superset/security/manager.py`,
`SupersetSecurityManager.on_user_login_failed` is defined at lines 578–582 to accept
`user: Any` and passes `{"username": user.username, "user_id": user.id}` to
`_log_audit_event("UserLoginFailed", ...)`.

2. Flask-AppBuilder 5.2.x (external dependency required by this PR) calls
`SecurityManager.on_user_login_failed` with the attempted username string on
authentication failure (no `User` object exists when login fails), so in production this
override will be invoked with a `str`, not a `User`.

3. When a user submits invalid credentials to the login endpoint (handled by FAB's auth
views using `SupersetSecurityManager` as the `security_manager_class`), FAB calls
`on_user_login_failed(username)`, and Superset's override tries to access `user.username`
and `user.id`, raising `AttributeError` on the string; the failed-login request returns a
500 and the audit event is never logged.

4. The unit test `test_on_user_login_failed_logs_event` in
`tests/unit_tests/security/audit_log_test.py` lines 225–235 instead constructs `user =
MagicMock(spec=User)` (lines 228–230), calls `sm.on_user_login_failed(user)` (line 232),
and asserts a payload with both `"username"` and `"user_id"` (line 235), thereby
validating a non-existent contract and letting the crash and wrong payload go undetected;
updating the test to pass a username string and assert `{"username": "testuser"}` would
align with FAB's hook and reveal the real bug.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/security/audit_log_test.py
**Line:** 228:234
**Comment:**
	*Type Error: The failed-login hook in FAB is triggered with the attempted username (not an authenticated user object), so testing it with a `User` mock validates the wrong contract and can miss production failures. Update the test to pass a username string and assert the corresponding payload.

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.
👍 | 👎

Comment on lines +579 to +580
_log_audit_event(
"UserLoginFailed",
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 hook assumes the argument always has .username and .id, but FAB can pass None (unknown user) or a plain username string for failed auth attempts. Accessing attributes unconditionally will raise AttributeError and can turn a normal login failure into a 500 error. Handle both object and string/None inputs before building the payload. [null pointer]

Severity Level: Critical 🚨
- ❌ Failed login attempts return 500 instead of 401 response.
- ⚠️ Audit logging misses some failed authentication attempts.
Suggested change
_log_audit_event(
"UserLoginFailed",
username = user if isinstance(user, str) else getattr(user, "username", None)
user_id = getattr(user, "id", None)
_log_audit_event(
"UserLoginFailed",
{"username": username, "user_id": user_id},
Steps of Reproduction ✅
1. Superset config uses `SupersetSecurityManager` as the security manager via
`superset/security/__init__.py:17`, which exposes `SupersetSecurityManager` to
Flask-AppBuilder.

2. Authentication views are registered in `superset/security/manager.py:3277-3283`, wiring
`SupersetAuthView` from `superset/views/auth.py:4-13` to handle the `/login` route in
normal authentication flows.

3. Trigger a failed login for a non-existent or invalid user by POSTing bad credentials to
`/login` so that Flask-AppBuilder's auth stack invokes
`SupersetSecurityManager.on_user_login_failed(user)` with a value that is not a `User`
instance (for failed auth attempts FAB can pass `None` or the attempted username string
instead of a model object).

4. The implementation at `superset/security/manager.py:578-582` assumes `user` always has
`.username` and `.id` and executes `user.username` and `user.id` unconditionally, which
raises `AttributeError` when `user` is `None` or a string; this turns a normal login
failure into a 500 error and prevents the `UserLoginFailed` audit event from being
recorded by the configured `EVENT_LOGGER`.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/security/manager.py
**Line:** 579:580
**Comment:**
	*Null Pointer: The failed-login hook assumes the argument always has `.username` and `.id`, but FAB can pass `None` (unknown user) or a plain username string for failed auth attempts. Accessing attributes unconditionally will raise `AttributeError` and can turn a normal login failure into a 500 error. Handle both object and string/None inputs before building the payload.

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.
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a gap on FAB only for AUTH_DB. Will address this later, non critical for now

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review Agent Run #0f74e9

Actionable Suggestions - 2
  • superset/utils/log.py - 1
    • Refactor to use transaction context manager · Line 415-415
  • superset/security/manager.py - 1
    • Blind exception catch without specific type · Line 148-148
Additional Suggestions - 1
  • superset/security/manager.py - 1
    • Inconsistent user attribute access · Line 572-582
      The on_user_logout method uses getattr to safely access user attributes, but on_user_login and on_user_login_failed access them directly. This risks AttributeError if the user object lacks username or id attributes. For consistency and robustness, update the login methods to use getattr like logout does.
      Code suggestion
       @@ -572,11 +572,11 @@
      -    def on_user_login(self, user: Any) -> None:
      -        _log_audit_event(
      -            "UserLoggedIn",
      -            {"username": user.username, "user_id": user.id},
      -        )
      -
      -    def on_user_login_failed(self, user: Any) -> None:
      -        _log_audit_event(
      -            "UserLoginFailed",
      -            {"username": user.username, "user_id": user.id},
      -        )
      -
      +    def on_user_login(self, user: Any) -> None:
      +        _log_audit_event(
      +            "UserLoggedIn",
      +            {"username": getattr(user, "username", None), "user_id": getattr(user, "id", None)},
      +        )
      +
      +    def on_user_login_failed(self, user: Any) -> None:
      +        _log_audit_event(
      +            "UserLoginFailed",
      +            {"username": getattr(user, "username", None), "user_id": getattr(user, "id", None)},
      +        )
      +
Review Details
  • Files reviewed - 7 · Commit Range: edceaa6..db88a17
    • pyproject.toml
    • requirements/base.txt
    • requirements/development.txt
    • superset/security/manager.py
    • superset/utils/log.py
    • tests/unit_tests/security/api_test.py
    • tests/unit_tests/security/audit_log_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ 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

@dpgaspar dpgaspar requested a review from Vitor-Avila April 9, 2026 12:44
@dpgaspar dpgaspar merged commit 5815665 into apache:master Apr 9, 2026
67 checks passed
@dpgaspar dpgaspar deleted the danielgaspar/sc-102915/visa-pcs-include-fab-security-events-on-superset branch April 9, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authentication Related to authentication logging Creates a UI or API endpoint that could benefit from logging. review:draft size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants