Hello we have a fork using Superset version 3.1.1, we tried creating a custom DbEventLogger
and was able to put custom code in actions but there is no trigger for User Management actions (Ex: Add user , edit User etc..)
we are planning to make User Management Security Auditing for (CRUD) operations, would appreciate help on how we could make logging actions on user management
thank you!
# Inside `superset_config.py`
class CustomUserEventLogger(DBEventLogger):
def log(self, user_id, action, *args, **kwargs):
print(f"USER: {user_id} >>>>>>>> PERFORMED : {action} <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
# You can add custom logic here to filter for user management actions
if action in ["user_create", "user_update", "user_delete"]:
# Add your custom logging logic here
print(f"User management action: {action}, user_id: {user_id}, details: {kwargs}")
# Call the parent logger to keep default behavior
super().log(user_id, action, *args, **kwargs)
EVENT_LOGGER = CustomUserEventLogger()
Originally posted by @LeandroMartinMacato in https://github.com/apache/superset/discussions/33538#discussioncomment-14642292
Comment From: dosubot[bot]
Superset 3.1.1 does not provide built-in event triggers for user management CRUD actions like add, edit, or delete user. These actions are handled mainly in the SupersetUserApi
and SupersetSecurityManager
classes, which use Flask AppBuilder (FAB) views for user management, but do not emit log events for these operations by default reference.
To implement auditing for user management actions, you’ll need to instrument the relevant methods yourself. The most maintainable approaches are:
- Subclass or monkey-patch the relevant FAB views or API classes (like
SupersetUserApi
orUserView
) and add calls to your custom logger in the methods that handle user creation, update, and deletion. - Use SQLAlchemy model events (such as
after_insert
,after_update
,after_delete
) on the User model to trigger logging when user records are changed reference.
For example, you could add a SQLAlchemy event listener in your superset_config.py
:
from sqlalchemy import event
from flask_appbuilder.security.sqla.models import User
def log_user_event(mapper, connection, target):
# Replace with your custom logging logic
EVENT_LOGGER.log(target.id, "user_update", details={"username": target.username})
event.listen(User, 'after_update', log_user_event)
event.listen(User, 'after_insert', log_user_event)
event.listen(User, 'after_delete', log_user_event)
Or, if you prefer to patch the view logic, override the relevant methods in your custom view and call your logger there.
This approach keeps your changes outside the core Superset code, making upgrades easier. Just make sure to test after each upgrade, as internal APIs may change.
Let me know if you want more detailed examples for a specific method or integration!
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Comment From: LeandroMartinMacato
@dosu this works now i can trigger custom code for User Management Actions, could you give me detailed examples on how I could integrate this to Supersets Action Log and also is it possible to get the User's IP Address for auditing purposes?
Comment From: dosubot[bot]
You can integrate your custom user management action logging directly into Superset’s Action Log by using your custom DbEventLogger
to write entries to the logs table. The logs table supports fields like action
, user_id
, json
(for extra details), and referrer
(which you can use for the IP address) reference.
Here’s a detailed example using SQLAlchemy events and Flask’s request context to capture user CRUD actions and log the user’s IP address:
# In superset_config.py or your custom extension
from sqlalchemy import event
from flask import request
from flask_appbuilder.security.sqla.models import User
def log_user_event(mapper, connection, target):
# Safely get the user's IP address from the request context
ip_addr = None
if request:
# Prefer X-Forwarded-For if behind a proxy, else use remote_addr
ip_addr = request.headers.get('X-Forwarded-For', request.remote_addr)
# Choose the action type based on the event
action = "user_update" # or "user_create"/"user_delete" depending on the event
# Build extra details for auditing
details = {
"username": target.username,
"email": getattr(target, "email", None),
"ip_address": ip_addr,
}
# Log to Superset's Action Log via your custom event logger
EVENT_LOGGER.log(
user_id=target.id,
action=action,
json=details, # This will be serialized and stored in the logs table
referrer=ip_addr, # Optionally store IP in the referrer field
)
event.listen(User, 'after_update', log_user_event)
event.listen(User, 'after_insert', log_user_event)
event.listen(User, 'after_delete', log_user_event)
- The
json
field in the logs table can store any extra metadata you want (like username, email, IP address, etc.) reference. - The
referrer
field can be used to store the IP address for easy filtering in the Action Log UI reference. - You can filter and view these logs in the Action Log UI by action, user, or any custom details you include reference.
This pattern is upgrade-safe since it uses SQLAlchemy events and the standard logging infrastructure. If you patch view logic instead, you can use the @EVENT_LOGGER.log_this_with_context(action="user_create")
decorator on your custom methods to automatically capture request context and log entries reference.
Let me know if you want an example for patching view logic or have questions about customizing the log format!
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Comment From: LeandroMartinMacato
@dosu
I created a seperate file for my custom DbEventLogger Class and refer to it in superset_config.py
, This is now able to trigger the specific event , however getting this error in the user management page This transaction is closed
when logging into superset's logging table
EVENT_LOGGER = CustomEventLogger
custom_db_event_logger.py
from superset.utils.log import DBEventLogger
from sqlalchemy import event
from flask_appbuilder.security.sqla.models import User
from custom_sso_security_manager import CustomUser
from flask import has_request_context, request, g
class CustomEventLogger(DBEventLogger):
def __init__(self):
print(">>>>> Initializing CustomUserEvent Logger <<<<<")
# Listen to Meta Database Update then audit movement
event.listen(CustomUser, 'after_update', self.log_user_edit)
event.listen(CustomUser, 'after_insert', self.log_user_insert)
event.listen(CustomUser, 'after_delete', self.log_user_delete)
def log_user_insert(self, mapper, connection, target):
print("\n\n>>>>>>> USER MANAGEMENT INSERT USER TRIGGER <<<<<<<<<<<<<<<<\n\n")
self.log(actor.id, "user_insert", dashboard_id=None,
duration_ms=None, slice_id=None, referrer=None)
def log_user_delete(self, mapper, connection, target):
print("\n\n>>>>>>> USER MANAGEMENT DELETE USER TRIGGER <<<<<<<<<<<<<<<<\n\n")
self.log(actor.id, "user_delete", dashboard_id=None,
duration_ms=None, slice_id=None, referrer=None)
def log_user_edit(self, mapper, connection, target):
# Replace with your custom logging logic
print("\n\n>>>>>>> USER MANAGEMENT EDIT USER TRIGGER <<<<<<<<<<<<<<<<\n\n")
print(type(target))
print(target)
actor = getattr(g, "user", None)
username = getattr(actor, "username", "system")
print(f'ACTOR: {actor} \n Actor Username: {username}')
ip_address = None
if has_request_context():
ip_address = request.headers.get(
"X-Forwarded-For", request.remote_addr)
print(f'IP ADDRESS: {ip_address}')
# self.log(target.id, "user_update", details={"username": target.username})
details = {
"username": username,
"ip_address": ip_address,
"actor_id": actor.id if actor else None,
}
# self.log(actor.id, "user_edit", dashboard_id=None, duration_ms=None,
# slice_id=None, referrer=None, **{"records": [details]})
# self.log(actor.id, "user_edit", dashboard_id=None, duration_ms=None,
# slice_id=None, referrer=None)
self.log(actor.id, "user_edit", *args, **kwargs)
def log(self, user_id, action, *args, **kwargs):
# print(f"USER: {user_id} >>>>>>>> PERFORMED : {action} <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
# # You can add custom logic here to filter for user management actions
# if action in ["user_create", "user_update", "user_delete"]:
# # Add your custom logging logic here
# print(f"User management action: {action}, user_id: {user_id}, details: {kwargs}")
# # Call the parent logger to keep default behavior
super().log(user_id, action, *args, **kwargs)
Comment From: dosubot[bot]
The error happens because you're trying to write to Superset's logs table from inside SQLAlchemy event listeners (like after_insert, after_update, after_delete), which run during the flush/commit phase of a transaction. At that point, the session may be in a closed or flushing state, so any new database writes (like logging) can trigger "This transaction is closed" errors see code.
The recommended pattern is to avoid logging to the database directly from model event listeners. Instead, use hooks provided by Flask AppBuilder (FAB) views, such as after_model_change
, after_model_create
, or after_model_delete
, which are called after the transaction is safely committed. These hooks are designed for side effects like auditing and logging, and operate in a stable transaction context see note.
If you need to log user management actions, subclass the relevant FAB ModelView (for example, UserView), override the appropriate hook, and call your logger there. This approach avoids transaction errors and ensures the request context (for IP address, actor, etc.) is available.
If you want a concrete example for patching the view logic or using FAB hooks, let me know!
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Comment From: LeandroMartinMacato
Show me an example how to implement this @dosu
Comment From: dosubot[bot]
Here's a concrete example for robust user management action logging in Superset, avoiding transaction errors and capturing audit details like actor and IP address:
Subclass the relevant API class (e.g., SupersetUserApi
or UserRestApi
), override the CRUD methods (post
, put
, delete
), and use the event_logger.log_this_with_context
decorator to log actions with full request context. This ensures logging happens after the transaction is committed and avoids the "transaction is closed" error you saw with SQLAlchemy event listeners reference.
Example:
from flask import g, request
from superset.extensions import event_logger
from superset.views.users.api import UserRestApi
class CustomUserRestApi(UserRestApi):
@event_logger.log_this_with_context(
action="user_create",
log_to_statsd=False,
)
def post(self):
# Your user creation logic here
response = super().post()
# Optionally add extra audit details
actor = getattr(g, "user", None)
ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
event_logger.log(
user_id=actor.id if actor else None,
action="user_create",
json={
"actor_username": getattr(actor, "username", None),
"ip_address": ip_address,
"details": "User created",
},
referrer=ip_address,
)
return response
@event_logger.log_this_with_context(
action="user_update",
log_to_statsd=False,
)
def put(self, user_id):
# Your user update logic here
response = super().put(user_id)
actor = getattr(g, "user", None)
ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
event_logger.log(
user_id=actor.id if actor else None,
action="user_update",
json={
"actor_username": getattr(actor, "username", None),
"ip_address": ip_address,
"details": f"User {user_id} updated",
},
referrer=ip_address,
)
return response
@event_logger.log_this_with_context(
action="user_delete",
log_to_statsd=False,
)
def delete(self, user_id):
# Your user deletion logic here
response = super().delete(user_id)
actor = getattr(g, "user", None)
ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
event_logger.log(
user_id=actor.id if actor else None,
action="user_delete",
json={
"actor_username": getattr(actor, "username", None),
"ip_address": ip_address,
"details": f"User {user_id} deleted",
},
referrer=ip_address,
)
return response
This pattern is upgrade-safe and integrates with Superset's Action Log. You can register your custom API class in your fork or extension by replacing the default view registration for user management.
The event_logger.log_this_with_context
decorator automatically captures request context and user info, and you can add extra details to the log entry as shown above reference.
Let me know if you need help with registering your custom API or want an example for a different method!
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Comment From: LeandroMartinMacato
I think this will not be called when user CRUD is performed on superset User Management, and the reference isnt linked to apache superset version 3.1.1
with that tag name
I would like to stick with sqlalchemy event listener, is it possible to just create another transaction for the log to make it simpler and not touch too much superset code for ease of version upgrade? @dosu
Comment From: dosubot[bot]
It's technically possible to open a new SQLAlchemy session inside your event listener to log user management actions, which can avoid the "transaction is closed" error you see when using Superset's main session during flush/commit phases (DBEventLogger code). For example, you could do something like:
from sqlalchemy.orm import sessionmaker
from superset import db
from superset.models.core import Log
def log_user_event(mapper, connection, target):
Session = sessionmaker(bind=db.engine)
session = Session()
try:
log = Log(
action="user_update",
user_id=target.id,
json='{"username": "%s"}' % target.username,
)
session.add(log)
session.commit()
finally:
session.close()
However, this approach is not officially recommended by Superset and comes with risks: you may run into transactional boundary issues, deadlocks, or data loss if the main transaction fails but your log is already committed. You should thoroughly test and monitor this if you go this route (core model code).
The upgrade-safe and supported pattern is to use hooks on Flask AppBuilder views (like after_model_change
), which run after the transaction is committed and are designed for side-effect logging. If you stick with event listeners and a new session, keep your implementation isolated and be aware of the risks.
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Comment From: LeandroMartinMacato
There is no UserRestApi
on version 3.1.1
kindly verify this, also will this work if User CRUD actions is performed on the frontend side of Superset @dosu
Comment From: dosubot[bot]
You're correct—UserRestApi
is not the main handler for user management in Superset 3.1.1. User CRUD actions performed in the frontend (like adding, editing, or deleting users) are always routed to backend Flask AppBuilder views and APIs, such as SupersetUserApi
and various user model views (e.g., UserDBModelView
, UserLDAPModelView
, etc.) see source.
This means any backend hooks you set up—like SQLAlchemy event listeners or Flask AppBuilder view hooks—will be triggered by frontend user management actions. If you use SQLAlchemy event listeners for logging, you must open a new SQLAlchemy session for your log writes to avoid transaction errors, but this approach is not officially recommended and can risk deadlocks or data loss if the main transaction fails see source.
The upgrade-safe way is to use Flask AppBuilder view hooks (after_model_change
, etc.), but if you stick with event listeners and a separate session, your logging will still capture all frontend-initiated CRUD actions since they're processed by the backend views.
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Comment From: LeandroMartinMacato
I am curious how would you implement a custom UserDbModelView
in apache superset 3.1.1 implementing a logging on CRUD operations @dosu
Comment From: dosubot[bot]
To implement logging for user CRUD operations in Superset 3.1.1, subclass UserDBModelView
and override the Flask AppBuilder hooks: after_model_create
, after_model_change
, and after_model_delete
. These hooks are called after the transaction is committed, so you can safely log actions without running into transaction errors. You can access the actor via g.user
and the request context for the IP address.
Here's a concrete example:
from flask import g, request
from superset.utils.log import DBEventLogger
from flask_appbuilder.security.views import UserDBModelView
class CustomUserDBModelView(UserDBModelView):
def after_model_create(self, model):
actor = getattr(g, "user", None)
ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
DBEventLogger().log(
user_id=actor.id if actor else None,
action="user_create",
dashboard_id=None,
duration_ms=None,
slice_id=None,
referrer=ip_address,
records=[{
"actor_username": getattr(actor, "username", None),
"target_username": getattr(model, "username", None),
"ip_address": ip_address,
"details": "User created",
}],
)
def after_model_change(self, model):
actor = getattr(g, "user", None)
ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
DBEventLogger().log(
user_id=actor.id if actor else None,
action="user_update",
dashboard_id=None,
duration_ms=None,
slice_id=None,
referrer=ip_address,
records=[{
"actor_username": getattr(actor, "username", None),
"target_username": getattr(model, "username", None),
"ip_address": ip_address,
"details": "User updated",
}],
)
def after_model_delete(self, model):
actor = getattr(g, "user", None)
ip_address = request.headers.get("X-Forwarded-For", request.remote_addr)
DBEventLogger().log(
user_id=actor.id if actor else None,
action="user_delete",
dashboard_id=None,
duration_ms=None,
slice_id=None,
referrer=ip_address,
records=[{
"actor_username": getattr(actor, "username", None),
"target_username": getattr(model, "username", None),
"ip_address": ip_address,
"details": "User deleted",
}],
)
To use your custom view, register it in your security manager or appbuilder setup, replacing the default UserDBModelView
. This approach is upgrade-safe and ensures all frontend user management actions are logged via the backend view hooks reference.
Let me know if you need details on registration or want to customize the log format further!
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other