diff --git a/corporate/tests/test_activity_views.py b/corporate/tests/test_activity_views.py index 19e7adea04..1cd05f1003 100644 --- a/corporate/tests/test_activity_views.py +++ b/corporate/tests/test_activity_views.py @@ -213,6 +213,10 @@ class ActivityTest(ZulipTestCase): result = self.client_get(f"/activity/plan_ledger/{plan.id}/") self.assertEqual(result.status_code, 200) + with self.assert_database_query_count(7): + result = self.client_get(f"/activity/remote/logs/server/{server.uuid}/") + self.assertEqual(result.status_code, 200) + def test_get_remote_server_guest_and_non_guest_count(self) -> None: RemoteRealmAuditLog.objects.bulk_create([RemoteRealmAuditLog(**data) for data in data_list]) server_id = 1 @@ -374,3 +378,7 @@ class ActivityTest(ZulipTestCase): with self.assert_database_query_count(11): result = self.client_get("/activity/remote") self.assertEqual(result.status_code, 200) + + with self.assert_database_query_count(7): + result = self.client_get(f"/activity/remote/logs/server/{remote_server.uuid}/") + self.assertEqual(result.status_code, 200) diff --git a/corporate/urls.py b/corporate/urls.py index b61ccb60f3..4c291f7898 100644 --- a/corporate/urls.py +++ b/corporate/urls.py @@ -4,6 +4,7 @@ from django.conf.urls import include from django.urls import path from django.views.generic import RedirectView, TemplateView +from corporate.views.audit_logs import get_remote_server_logs from corporate.views.billing_page import ( billing_page, remote_realm_billing_page, @@ -106,6 +107,7 @@ i18n_urlpatterns: Any = [ path("user_activity//", get_user_activity), path("activity/remote", get_remote_server_activity), path("activity/remote/support", remote_servers_support, name="remote_servers_support"), + path("activity/remote/logs/server//", get_remote_server_logs), path("activity/plan_ledger//", get_plan_ledger), ] diff --git a/corporate/views/audit_logs.py b/corporate/views/audit_logs.py new file mode 100644 index 0000000000..8868249f19 --- /dev/null +++ b/corporate/views/audit_logs.py @@ -0,0 +1,102 @@ +from typing import Any + +from django.http import HttpRequest, HttpResponse, HttpResponseNotFound +from django.shortcuts import render + +from corporate.lib.activity import ActivityHeaderEntry, format_optional_datetime, make_table +from zerver.decorator import require_server_admin +from zerver.lib.typed_endpoint import PathOnly +from zerver.models.realm_audit_logs import AbstractRealmAuditLog, AuditLogEventType +from zilencer.models import RemoteRealmAuditLog, RemoteZulipServer, RemoteZulipServerAuditLog + +USER_ROLES_KEY = "100: owner, 200: admin, 300: moderator, 400: member, 600: guest" + + +def get_remote_realm_host(audit_log: RemoteRealmAuditLog) -> str: + if audit_log.remote_realm is None: + # For pre-8.0 servers, we might only have the realm ID and thus + # no RemoteRealm object yet, so we show that information instead. + return f"N/A, realm ID: {audit_log.realm_id}" + return audit_log.remote_realm.host + + +def get_human_role_count_data(audit_log: RemoteRealmAuditLog | RemoteZulipServerAuditLog) -> str: + extra_data = audit_log.extra_data + role_count = extra_data.get(AbstractRealmAuditLog.ROLE_COUNT, {}) + human_count_raw: dict[str, Any] = role_count.get(AbstractRealmAuditLog.ROLE_COUNT_HUMANS, {}) + if human_count_raw == {}: + return "N/A" + human_count_string = "" + for role, count in human_count_raw.items(): + if int(count) > 0: + human_count_string += f"{(role)}: {count}, " + return human_count_string.strip(", ") + + +@require_server_admin +def get_remote_server_logs(request: HttpRequest, *, uuid: PathOnly[str]) -> HttpResponse: + try: + remote_server = RemoteZulipServer.objects.get(uuid=uuid) + except RemoteZulipServer.DoesNotExist: + return HttpResponseNotFound() + + remote_server_audit_logs = RemoteZulipServerAuditLog.objects.filter( + server=remote_server + ).order_by("-id") + remote_realm_audit_logs = ( + RemoteRealmAuditLog.objects.filter(server=remote_server) + .order_by("-id") + .select_related("remote_realm") + ) + + title = f"{remote_server.hostname}" + cols = [ + "Event time", + "Event type", + "Audit log ID", + "Remote realm host", + "Role count: human", + ] + + def row(audit_log: RemoteRealmAuditLog | RemoteZulipServerAuditLog) -> list[Any]: + return [ + audit_log.event_time, + AuditLogEventType(audit_log.event_type).name, + audit_log.id if isinstance(audit_log, RemoteRealmAuditLog) else f"S{audit_log.id}", + get_remote_realm_host(audit_log) if isinstance(audit_log, RemoteRealmAuditLog) else "", + get_human_role_count_data(audit_log) + if audit_log.event_type in AbstractRealmAuditLog.SYNCED_BILLING_EVENTS + else "", + ] + + remote_server_audit_log_rows = list(map(row, remote_server_audit_logs)) + remote_realm_audit_log_rows = list(map(row, remote_realm_audit_logs)) + rows = remote_server_audit_log_rows + remote_realm_audit_log_rows + + header_entries = [] + if remote_server.last_version is not None: + header_entries.append( + ActivityHeaderEntry( + name="Zulip version", + value=remote_server.last_version, + ) + ) + header_entries.append( + ActivityHeaderEntry( + name="Last audit log update", + value=format_optional_datetime(remote_server.last_audit_log_update), + ) + ) + header_entries.append(ActivityHeaderEntry(name="Role key", value=USER_ROLES_KEY)) + + content = make_table(title, cols, rows, header=header_entries) + + return render( + request, + "corporate/activity/activity.html", + context=dict( + data=content, + title=title, + is_home=False, + ), + ) diff --git a/templates/corporate/support/remote_server_support.html b/templates/corporate/support/remote_server_support.html index 6db26e5120..d472255e91 100644 --- a/templates/corporate/support/remote_server_support.html +++ b/templates/corporate/support/remote_server_support.html @@ -76,6 +76,7 @@ Plan type: {{ get_plan_type_name(remote_server.plan_type) }}
Non-guest user count: {{ remote_servers_support_data[remote_server.id].user_data.non_guest_user_count }}
Guest user count: {{ remote_servers_support_data[remote_server.id].user_data.guest_user_count }}
+ View audit logs

Total mobile user count: {{ remote_servers_support_data[remote_server.id].mobile_push_data.total_mobile_users }}
{% if remote_realms[remote_server.id] != [] %} diff --git a/tools/test-backend b/tools/test-backend index c48bff0918..07c06dcf42 100755 --- a/tools/test-backend +++ b/tools/test-backend @@ -57,6 +57,7 @@ not_yet_fully_covered = [ "corporate/views/plan_activity.py", "corporate/views/realm_activity.py", "corporate/views/remote_billing_page.py", + "corporate/views/audit_logs.py", "corporate/views/support.py", "corporate/lib/activity.py", "corporate/lib/remote_billing_util.py",