Skip to content

Commit fb4131c

Browse files
varmar05claude
andcommitted
Audit log fixes
project delete, login failed target, invitation events, spurious project.updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8f9f99a commit fb4131c

6 files changed

Lines changed: 52 additions & 7 deletions

File tree

‎server/mergin/auth/controller.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,11 @@ def login_public(): # noqa: E501
173173
)
174174
return data
175175
else:
176+
audit_user = user or User.get_by_login(form.login.data)
176177
emit(
177178
AuthEventType.USER_LOGIN_FAILED,
178179
**request_context(),
179-
target_user_id=user.id if user else None,
180+
target_user_id=audit_user.id if audit_user else None,
180181
login=form.login.data,
181182
reason="account_inactive" if user else "invalid_credentials",
182183
login_method="password",
@@ -267,10 +268,11 @@ def login(): # pylint: disable=W0613,W0612
267268
)
268269
return "", 200
269270
else:
271+
audit_user = user or User.get_by_login(form.login.data)
270272
emit(
271273
AuthEventType.USER_LOGIN_FAILED,
272274
**request_context(),
273-
target_user_id=user.id if user else None,
275+
target_user_id=audit_user.id if audit_user else None,
274276
login=form.login.data,
275277
reason="account_inactive" if user else "invalid_credentials",
276278
login_method="password",
@@ -310,10 +312,11 @@ def admin_login(): # pylint: disable=W0613,W0612
310312
)
311313
abort(403, "You do not have permissions")
312314
else:
315+
audit_user = User.get_by_login(form.login.data)
313316
emit(
314317
AuthEventType.USER_LOGIN_FAILED,
315318
**request_context(),
316-
target_user_id=None,
319+
target_user_id=audit_user.id if audit_user else None,
317320
login=form.login.data,
318321
reason="invalid_credentials",
319322
login_method="password",

‎server/mergin/sync/models.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,6 @@ def delete(self, removed_by: int = None):
366366
target_workspace_id=self.workspace_id,
367367
workspace_name=self.workspace.name,
368368
project_name=project_name,
369-
triggered_by=db.session.info.get("audit_project_deletion_source"),
370369
)
371370
project_deleted.send(self)
372371

‎server/mergin/sync/private_api_controller.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,9 @@ def unsubscribe_project(id): # pylint: disable=W0612
344344

345345
project.unset_role(current_user.id)
346346
db.session.add(project)
347+
db.session.info["project_member_delete_reason"] = "left"
347348
db.session.commit()
349+
db.session.info.pop("project_member_delete_reason", None)
348350
return NoContent, 200
349351

350352

‎server/mergin/sync/public_api_controller.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from .interfaces import WorkspaceRole
3737
from ..app import db
3838
from ..audit import emit
39-
from ..audit.listeners import actor_context
39+
from ..audit.listeners import actor_context, audit_session_flags
4040
from ..auth import auth_required
4141
from ..auth.models import User
4242
from .events import SyncEventType
@@ -309,7 +309,19 @@ def delete_project(namespace, project_name): # noqa: E501
309309
:rtype: None
310310
"""
311311
project = require_project(namespace, project_name, ProjectPermissions.Delete)
312-
project.schedule_deletion(removed_by=current_user.id)
312+
with audit_session_flags(db.session, audit_skip_project_update=True):
313+
project.schedule_deletion(removed_by=current_user.id)
314+
emit(
315+
SyncEventType.PROJECT_MARKED_FOR_DELETION,
316+
**actor_context(),
317+
target_project_id=project.id,
318+
target_workspace_id=project.workspace_id,
319+
workspace_name=project.workspace.name,
320+
project_name=f"{project.workspace.name}/{project.name}",
321+
scheduled_for_deletion_at=(
322+
project.removed_at.isoformat() if project.removed_at else None
323+
),
324+
)
313325
return NoContent, 200
314326

315327

‎server/mergin/sync/tasks.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def remove_projects_backups():
7070
with audit_session_flags(
7171
db.session,
7272
audit_skip_project_update=True,
73-
audit_project_deletion_source="system",
7473
):
7574
for p in projects:
7675
p.delete()

‎server/mergin/tests/test_audit_events.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,20 @@ def test_project_marked_for_deletion(client, audit_capture):
293293
assert e.target_workspace_id == project.workspace_id
294294

295295

296+
def test_project_marked_for_deletion_via_v1_api(client, audit_capture):
297+
"""Deleting a project via the legacy v1 API (used by QGIS plugin) emits PROJECT_MARKED_FOR_DELETION."""
298+
project = Project.query.filter_by(
299+
workspace_id=test_workspace_id, name=test_project
300+
).first()
301+
workspace_name = project.workspace.name
302+
303+
client.delete(f"/v1/project/{workspace_name}/{project.name}")
304+
305+
e = audit_capture.one(SyncEventType.PROJECT_MARKED_FOR_DELETION)
306+
assert e.target_project_id == project.id
307+
assert e.target_workspace_id == project.workspace_id
308+
309+
296310
def test_project_restored(client, audit_capture):
297311
project = Project.query.filter_by(
298312
workspace_id=test_workspace_id, name=test_project
@@ -370,6 +384,22 @@ def test_project_member_deleted(client, audit_capture):
370384
assert e.metadata["reason"] == "removed"
371385

372386

387+
def test_project_member_left(client, audit_capture):
388+
project = Project.query.filter_by(
389+
workspace_id=test_workspace_id, name=test_project
390+
).first()
391+
user = add_user("leavemember", "pass123")
392+
project.set_role(user.id, ProjectRole.READER)
393+
db.session.commit()
394+
login(client, user.username, "pass123")
395+
396+
client.post(f"/app/project/unsubscribe/{project.id}")
397+
398+
e = audit_capture.one(SyncEventType.PROJECT_MEMBER_DELETED)
399+
assert e.metadata["target_email"] == user.email
400+
assert e.metadata["reason"] == "left"
401+
402+
373403
def test_project_access_request_created(client, audit_capture):
374404
project = Project.query.filter_by(
375405
workspace_id=test_workspace_id, name=test_project

0 commit comments

Comments
 (0)