Superset User Deletion#
Deleting a Superset user involves more than a single DELETE FROM ab_user. Several FK constraints and an ORM-level circular dependency can block the operation. Understanding the three distinct failure modes—role associations, audit FK violations, and self-referential circular dependencies—is essential for debugging or extending user deletion.
Entry Points#
User deletion is triggered through Settings → List Users in the GUI or via the FAB security API. Superset extends Flask-AppBuilder's UserApi with SupersetUserApi (registered as user_api on SupersetSecurityManager). The actual DB deletion is inherited from FAB's SecurityManager.delete_user(), which issues a bare DELETE FROM ab_user. UserDAO does not override deletion; it inherits BaseDAO.delete().
Failure Mode 1 — Role Association (ab_user_role)#
A user's role memberships are stored in the ab_user_role association table. FAB's default delete_user() does not clear these rows first, so deleting a user with roles can raise an IntegrityError on databases that enforce FK constraints.
Fix (already in place): SupersetUserApi.pre_delete() sets item.roles = [] before the delete. SQLAlchemy flushes the cleared association rows before issuing the DELETE FROM ab_user, letting the delete succeed.
Failure Mode 2 — Audit FK Violations (created_by_fk, changed_by_fk, and other user_id FKs)#
Every model that mixes in AuditMixin from superset/models/helpers.py carries nullable created_by_fk → ab_user.id and changed_by_fk → ab_user.id columns without an ON DELETE rule, so the database defaults to RESTRICT. Additional user_id FKs on tables like logs, saved_query, query, key_value, favstar, and tab_state share the same problem.
When any of those columns reference the user being deleted, the DB raises ForeignKeyViolation / IntegrityError .
Migrations that partially fix this:
6d05b0a70c89(2023) — addedON DELETE CASCADEfor ownership tables (dashboard_user,slice_user,report_schedule_user).6fbe660cac39(2023) — addedON DELETE CASCADEforsqlatable_user.
Still missing as of 6.1.0: ON DELETE SET NULL for audit columns (created_by_fk, changed_by_fk, logs.user_id, saved_query.user_id, query.user_id, key_value.created_by_fk) and ON DELETE CASCADE for favstar, user_attribute, tab_state. PR #41994 proposes a comprehensive migration and model-layer fix but was closed without merging . A community contributor confirmed the fix pattern and indicated a PR was incoming .
Failure Mode 3 — Self-Referential changed_by_fk → CircularDependencyError#
When a user edits their own profile via PUT /api/v1/me/, CurrentUserRestApi.pre_update() writes item.changed_by_fk = g.user.id — making the ab_user row's FK point to itself. FAB's User model declares created_by and changed_by as self-referential relationships without post_update=True. SQLAlchemy's unit-of-work cannot order the deletion of a row whose FK references itself and raises sqlalchemy.exc.CircularDependencyError .
Symptom: The API returns HTTP 422 with "Circular dependency detected" after any self-edit; the user appears in the list but cannot be deleted .
Workaround: Have an admin edit the affected user (e.g., deactivate and reactivate). This moves changed_by_fk off the user's own ID and allows deletion to proceed .
Root cause in code: pre_update in superset/views/users/api.py:48-50 unconditionally sets changed_by_fk to the current user's ID. SupersetUserApi.pre_delete() does not null out self-referencing changed_by_fk/created_by_fk .
Proposed fixes (none merged as of 6.1.0):
- Skip writing
changed_by_fkwhen the editor is the same user as the row being edited. - Null out self-referencing
changed_by_fk/created_by_fkinSupersetUserApi.pre_delete(). - Add
post_update=Trueto the self-referential relationships in Flask-AppBuilder.
Key Files#
| File | Purpose |
|---|---|
superset/security/manager.py:140-166 | SupersetUserApi + pre_delete (clears roles) |
superset/views/users/api.py:48-50 | pre_update that sets changed_by_fk = g.user.id |
superset/models/helpers.py:487-504 | created_by_fk / changed_by_fk column declarations (no ON DELETE) |
superset/daos/user.py | UserDAO — no delete override; inherits BaseDAO.delete() |
superset/daos/base.py:168-187 | BaseDAO.delete() — generic ORM delete |
Related Issues & PRs#
- Issue #44865 —
CircularDependencyErrorafter self-edit - Issue #38629 —
IntegrityErroron MariaDB/PostgreSQL due to missingON DELETErules - PR #41994 — Proposed
ON DELETE SET NULL/CASCADEmigration (closed, not merged) - PR #36342 — Proposed asset reassignment + soft/hard delete (closed, not merged)