mirror of
https://github.com/apache/superset.git
synced 2026-04-18 15:44:57 +00:00
feat: role/user CRUD events and login/logout tracking in the action log (#39121)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
6649f35a0d
commit
5815665cc6
@@ -32,7 +32,7 @@ def test_csrf_exempt_blueprints(app_context: None) -> None:
|
||||
are exempt from CSRF protection.
|
||||
"""
|
||||
assert {blueprint.name for blueprint in csrf._exempt_blueprints} == {
|
||||
"GroupApi",
|
||||
"SupersetGroupApi",
|
||||
"MenuApi",
|
||||
"SecurityApi",
|
||||
"OpenApi",
|
||||
|
||||
251
tests/unit_tests/security/audit_log_test.py
Normal file
251
tests/unit_tests/security/audit_log_test.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from flask_appbuilder.security.sqla.models import Group, Role, User
|
||||
|
||||
from superset.security.manager import (
|
||||
_log_audit_event,
|
||||
SupersetGroupApi,
|
||||
SupersetRoleApi,
|
||||
SupersetSecurityManager,
|
||||
SupersetUserApi,
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.extensions.event_logger")
|
||||
@patch("superset.security.manager.get_user_id", return_value=1)
|
||||
def test_log_audit_event_calls_event_logger(
|
||||
mock_get_user_id: MagicMock,
|
||||
mock_event_logger: MagicMock,
|
||||
) -> None:
|
||||
"""_log_audit_event delegates to the configured event_logger."""
|
||||
_log_audit_event("TestAction", {"key": "value"})
|
||||
|
||||
mock_event_logger.log.assert_called_once_with(
|
||||
user_id=1,
|
||||
action="TestAction",
|
||||
dashboard_id=None,
|
||||
duration_ms=None,
|
||||
slice_id=None,
|
||||
referrer=None,
|
||||
curated_payload=None,
|
||||
curated_form_data=None,
|
||||
records=[{"key": "value"}],
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.extensions.event_logger")
|
||||
@patch("superset.security.manager.get_user_id", return_value=1)
|
||||
def test_log_audit_event_handles_logger_error(
|
||||
mock_get_user_id: MagicMock,
|
||||
mock_event_logger: MagicMock,
|
||||
) -> None:
|
||||
"""_log_audit_event does not raise on event_logger errors."""
|
||||
mock_event_logger.log.side_effect = Exception("Logger error")
|
||||
# Should not raise
|
||||
_log_audit_event("TestAction", {"key": "value"})
|
||||
|
||||
|
||||
# --- Role CRUD ---
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_role_api_post_add_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetRoleApi.post_add logs a RoleCreated event."""
|
||||
api = SupersetRoleApi.__new__(SupersetRoleApi)
|
||||
role = MagicMock(spec=Role)
|
||||
role.name = "TestRole"
|
||||
role.id = 42
|
||||
api.post_add(role)
|
||||
mock_log.assert_called_once_with(
|
||||
"RoleCreated", {"role_name": "TestRole", "role_id": 42}
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_role_api_post_update_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetRoleApi.post_update logs a RoleUpdated event."""
|
||||
api = SupersetRoleApi.__new__(SupersetRoleApi)
|
||||
role = MagicMock(spec=Role)
|
||||
role.name = "TestRole"
|
||||
role.id = 42
|
||||
api.post_update(role)
|
||||
mock_log.assert_called_once_with(
|
||||
"RoleUpdated", {"role_name": "TestRole", "role_id": 42}
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_role_api_post_delete_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetRoleApi.post_delete logs a RoleDeleted event."""
|
||||
api = SupersetRoleApi.__new__(SupersetRoleApi)
|
||||
role = MagicMock(spec=Role)
|
||||
role.name = "TestRole"
|
||||
role.id = 42
|
||||
api.post_delete(role)
|
||||
mock_log.assert_called_once_with(
|
||||
"RoleDeleted", {"role_name": "TestRole", "role_id": 42}
|
||||
)
|
||||
|
||||
|
||||
# --- User CRUD ---
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_user_api_post_add_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetUserApi.post_add logs a UserCreated event."""
|
||||
api = SupersetUserApi.__new__(SupersetUserApi)
|
||||
user = MagicMock(spec=User)
|
||||
user.username = "testuser"
|
||||
user.id = 7
|
||||
user.email = "test@example.com"
|
||||
api.post_add(user)
|
||||
mock_log.assert_called_once_with(
|
||||
"UserCreated",
|
||||
{
|
||||
"target_username": "testuser",
|
||||
"target_user_id": 7,
|
||||
"email": "test@example.com",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_user_api_post_update_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetUserApi.post_update logs a UserUpdated event."""
|
||||
api = SupersetUserApi.__new__(SupersetUserApi)
|
||||
user = MagicMock(spec=User)
|
||||
user.username = "testuser"
|
||||
user.id = 7
|
||||
user.email = "test@example.com"
|
||||
user.active = True
|
||||
api.post_update(user)
|
||||
mock_log.assert_called_once_with(
|
||||
"UserUpdated",
|
||||
{
|
||||
"target_username": "testuser",
|
||||
"target_user_id": 7,
|
||||
"email": "test@example.com",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_user_api_post_delete_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetUserApi.post_delete logs a UserDeleted event."""
|
||||
api = SupersetUserApi.__new__(SupersetUserApi)
|
||||
user = MagicMock(spec=User)
|
||||
user.username = "testuser"
|
||||
user.id = 7
|
||||
api.post_delete(user)
|
||||
mock_log.assert_called_once_with(
|
||||
"UserDeleted",
|
||||
{"target_username": "testuser", "target_user_id": 7},
|
||||
)
|
||||
|
||||
|
||||
# --- Group CRUD ---
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_group_api_post_add_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetGroupApi.post_add logs a GroupCreated event."""
|
||||
api = SupersetGroupApi.__new__(SupersetGroupApi)
|
||||
group = MagicMock(spec=Group)
|
||||
group.name = "TestGroup"
|
||||
group.id = 10
|
||||
api.post_add(group)
|
||||
mock_log.assert_called_once_with(
|
||||
"GroupCreated", {"group_name": "TestGroup", "group_id": 10}
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_group_api_post_update_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetGroupApi.post_update logs a GroupUpdated event."""
|
||||
api = SupersetGroupApi.__new__(SupersetGroupApi)
|
||||
group = MagicMock(spec=Group)
|
||||
group.name = "TestGroup"
|
||||
group.id = 10
|
||||
api.post_update(group)
|
||||
mock_log.assert_called_once_with(
|
||||
"GroupUpdated", {"group_name": "TestGroup", "group_id": 10}
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_group_api_post_delete_logs_event(mock_log: MagicMock) -> None:
|
||||
"""SupersetGroupApi.post_delete logs a GroupDeleted event."""
|
||||
api = SupersetGroupApi.__new__(SupersetGroupApi)
|
||||
group = MagicMock(spec=Group)
|
||||
group.name = "TestGroup"
|
||||
group.id = 10
|
||||
api.post_delete(group)
|
||||
mock_log.assert_called_once_with(
|
||||
"GroupDeleted", {"group_name": "TestGroup", "group_id": 10}
|
||||
)
|
||||
|
||||
|
||||
# --- Login / Logout ---
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_on_user_login_logs_event(mock_log: MagicMock) -> None:
|
||||
"""on_user_login logs a UserLoggedIn event."""
|
||||
sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
|
||||
user = MagicMock(spec=User)
|
||||
user.username = "testuser"
|
||||
user.id = 7
|
||||
|
||||
sm.on_user_login(user)
|
||||
|
||||
mock_log.assert_called_once_with(
|
||||
"UserLoggedIn", {"username": "testuser", "user_id": 7}
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_on_user_login_failed_logs_event(mock_log: MagicMock) -> None:
|
||||
"""on_user_login_failed logs a UserLoginFailed event."""
|
||||
sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
|
||||
user = MagicMock(spec=User)
|
||||
user.username = "testuser"
|
||||
user.id = 7
|
||||
|
||||
sm.on_user_login_failed(user)
|
||||
|
||||
mock_log.assert_called_once_with(
|
||||
"UserLoginFailed", {"username": "testuser", "user_id": 7}
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.security.manager._log_audit_event")
|
||||
def test_on_user_logout_logs_event(mock_log: MagicMock) -> None:
|
||||
"""on_user_logout logs a UserLoggedOut event."""
|
||||
sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
|
||||
user = MagicMock(spec=User)
|
||||
user.username = "testuser"
|
||||
user.id = 7
|
||||
|
||||
sm.on_user_logout(user)
|
||||
|
||||
mock_log.assert_called_once_with(
|
||||
"UserLoggedOut", {"username": "testuser", "user_id": 7}
|
||||
)
|
||||
Reference in New Issue
Block a user