Users: deleting a user clears their API tokens and detaches audit rows
Deleting any user who owned an API token or appeared in the audit log hit the users FK and 500ed - the import's 'importer' account being the guaranteed case (its PAT plus every audit row the import wrote). Tokens are revoked outright; audit history is kept but detached (userid NULL), so the trail survives the account.
This commit is contained in:
@@ -212,6 +212,16 @@ def delete_user(userid: int):
|
|||||||
return error_response(ErrorCodes.NOT_FOUND, 'User not found', http_code=404)
|
return error_response(ErrorCodes.NOT_FOUND, 'User not found', http_code=404)
|
||||||
|
|
||||||
username = user.username
|
username = user.username
|
||||||
|
|
||||||
|
# Rows that reference the user would otherwise block the delete:
|
||||||
|
# revoke their API tokens outright, and DETACH their audit history
|
||||||
|
# (userid -> NULL) - the log rows themselves are kept, entityname and
|
||||||
|
# details still tell the story.
|
||||||
|
from shopdb.core.models import ApiToken
|
||||||
|
ApiToken.query.filter_by(userid=userid).delete(synchronize_session=False)
|
||||||
|
AuditLog.query.filter_by(userid=userid).update(
|
||||||
|
{'userid': None}, synchronize_session=False)
|
||||||
|
|
||||||
db.session.delete(user)
|
db.session.delete(user)
|
||||||
|
|
||||||
AuditLog.log('deleted', 'User', entityid=userid, entityname=username)
|
AuditLog.log('deleted', 'User', entityid=userid, entityname=username)
|
||||||
|
|||||||
29
tests/test_core/test_users_api.py
Normal file
29
tests/test_core/test_users_api.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
|
||||||
|
def test_delete_user_with_tokens_and_audit_history(client, auth_headers, app, db):
|
||||||
|
"""Deleting a user revokes their API tokens and detaches (not deletes)
|
||||||
|
their audit rows - the importer-user case."""
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
from shopdb.core.models import User, ApiToken, AuditLog
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
user = User(username='importer2', email='importer2@test.local',
|
||||||
|
passwordhash=generate_password_hash('x'), isactive=True)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.add(ApiToken(userid=user.userid, name='import token',
|
||||||
|
tokenprefix='deadbeef', tokenhash='x' * 64))
|
||||||
|
db.session.add(AuditLog(userid=user.userid, action='created',
|
||||||
|
entitytype='Asset', entityid=1,
|
||||||
|
entityname='imported thing'))
|
||||||
|
db.session.commit()
|
||||||
|
userid = user.userid
|
||||||
|
|
||||||
|
response = client.delete(f'/api/users/{userid}', headers=auth_headers)
|
||||||
|
assert response.status_code == 200, response.get_json()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
assert db.session.get(User, userid) is None
|
||||||
|
assert ApiToken.query.filter_by(userid=userid).count() == 0
|
||||||
|
detached = AuditLog.query.filter_by(entityname='imported thing').one()
|
||||||
|
assert detached.userid is None
|
||||||
Reference in New Issue
Block a user