remote_billing: Store acting users in remote user audit logs.

This commit is contained in:
Mateusz Mandera
2023-12-14 15:50:12 +01:00
committed by Tim Abbott
parent a13e42f18a
commit 651590c49a
4 changed files with 167 additions and 0 deletions

View File

@@ -3019,6 +3019,7 @@ class RemoteRealmBillingSession(BillingSession):
) -> None:
self.remote_realm = remote_realm
self.remote_billing_user = remote_billing_user
self.support_staff = support_staff
if support_staff is not None: # nocoverage
assert support_staff.is_staff
self.support_session = True
@@ -3119,6 +3120,10 @@ class RemoteRealmBillingSession(BillingSession):
"remote_realm": self.remote_realm,
"event_type": audit_log_event,
"event_time": event_time,
# At most one of these should be set, but we may
# not want an assert for that yet:
"acting_support_user": self.support_staff,
"acting_remote_user": self.remote_billing_user,
}
if extra_data:
@@ -3424,6 +3429,7 @@ class RemoteServerBillingSession(BillingSession):
) -> None:
self.remote_server = remote_server
self.remote_billing_user = remote_billing_user
self.support_staff = support_staff
if support_staff is not None: # nocoverage
assert support_staff.is_staff
self.support_session = True
@@ -3518,6 +3524,10 @@ class RemoteServerBillingSession(BillingSession):
"server": self.remote_server,
"event_type": audit_log_event,
"event_time": event_time,
# At most one of these should be set, but we may
# not want an assert for that yet:
"acting_support_user": self.support_staff,
"acting_remote_user": self.remote_billing_user,
}
if extra_data:

View File

@@ -19,7 +19,10 @@ from typing import (
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from unittest import mock
from unittest.mock import Mock, patch
@@ -109,6 +112,8 @@ from zilencer.lib.remote_counts import MissingDataError
from zilencer.models import (
RemoteRealm,
RemoteRealmAuditLog,
RemoteRealmBillingUser,
RemoteServerBillingUser,
RemoteZulipServer,
RemoteZulipServerAuditLog,
)
@@ -5432,3 +5437,98 @@ class TestSupportBillingHelpers(StripeTestCase):
self.assertEqual(success_message, "zulip downgraded and voided 1 open invoices")
original_plan.refresh_from_db()
self.assertEqual(original_plan.status, CustomerPlan.ENDED)
class TestRemoteBillingWriteAuditLog(StripeTestCase):
def test_write_audit_log(self) -> None:
support_admin = self.example_user("iago")
server_uuid = str(uuid.uuid4())
remote_server = RemoteZulipServer.objects.create(
uuid=server_uuid,
api_key="magic_secret_api_key",
hostname="demo.example.com",
contact_email="email@example.com",
)
realm_uuid = str(uuid.uuid4())
remote_realm = RemoteRealm.objects.create(
server=remote_server,
uuid=realm_uuid,
uuid_owner_secret="dummy-owner-secret",
host="dummy-hostname",
realm_date_created=timezone_now(),
)
remote_realm_billing_user = RemoteRealmBillingUser.objects.create(
remote_realm=remote_realm, email="admin@example.com", user_uuid=uuid.uuid4()
)
remote_server_billing_user = RemoteServerBillingUser.objects.create(
remote_server=remote_server, email="admin@example.com"
)
event_time = timezone_now()
def assert_audit_log(
audit_log: Union[RemoteRealmAuditLog, RemoteZulipServerAuditLog],
acting_remote_user: Optional[Union[RemoteRealmBillingUser, RemoteServerBillingUser]],
acting_support_user: Optional[UserProfile],
event_type: int,
event_time: datetime,
) -> None:
self.assertEqual(audit_log.event_type, event_type)
self.assertEqual(audit_log.event_time, event_time)
self.assertEqual(audit_log.acting_remote_user, acting_remote_user)
self.assertEqual(audit_log.acting_support_user, acting_support_user)
for session_class, audit_log_class, remote_object, remote_user in [
(
RemoteRealmBillingSession,
RemoteRealmAuditLog,
remote_realm,
remote_realm_billing_user,
),
(
RemoteServerBillingSession,
RemoteZulipServerAuditLog,
remote_server,
remote_server_billing_user,
),
]:
# Necessary cast or mypy doesn't understand that we can use Django's
# model .objects. style queries on this.
audit_log_model = cast(
Union[Type[RemoteRealmAuditLog], Type[RemoteZulipServerAuditLog]], audit_log_class
)
assert isinstance(remote_user, (RemoteRealmBillingUser, RemoteServerBillingUser))
# No acting user:
session = session_class(remote_object)
session.write_to_audit_log(
# This "ordinary billing" event type value gets translated by write_to_audit_log
# into a RemoteRealmBillingSession.CUSTOMER_PLAN_CREATED or
# RemoteServerBillingSession.CUSTOMER_PLAN_CREATED value.
event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED,
event_time=event_time,
)
audit_log = audit_log_model.objects.latest("id")
assert_audit_log(
audit_log, None, None, audit_log_class.CUSTOMER_PLAN_CREATED, event_time
)
session = session_class(remote_object, remote_billing_user=remote_user)
session.write_to_audit_log(
event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED,
event_time=event_time,
)
audit_log = audit_log_model.objects.latest("id")
assert_audit_log(
audit_log, remote_user, None, audit_log_class.CUSTOMER_PLAN_CREATED, event_time
)
session = session_class(
remote_object, remote_billing_user=None, support_staff=support_admin
)
session.write_to_audit_log(
event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED,
event_time=event_time,
)
audit_log = audit_log_model.objects.latest("id")
assert_audit_log(
audit_log, None, support_admin, audit_log_class.CUSTOMER_PLAN_CREATED, event_time
)

View File

@@ -0,0 +1,47 @@
# Generated by Django 4.2.8 on 2023-12-14 14:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("zilencer", "0052_alter_remoterealm_plan_type_and_more"),
]
operations = [
migrations.AddField(
model_name="remoterealmauditlog",
name="acting_remote_user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="zilencer.remoterealmbillinguser",
),
),
migrations.AddField(
model_name="remoterealmauditlog",
name="acting_support_user",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="remotezulipserverauditlog",
name="acting_remote_user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="zilencer.remoteserverbillinguser",
),
),
migrations.AddField(
model_name="remotezulipserverauditlog",
name="acting_support_user",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
),
),
]

View File

@@ -261,6 +261,11 @@ class RemoteZulipServerAuditLog(AbstractRealmAuditLog):
server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
acting_remote_user = models.ForeignKey(
RemoteServerBillingUser, null=True, on_delete=models.SET_NULL
)
acting_support_user = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL)
@override
def __str__(self) -> str:
return f"{self.server!r} {self.event_type} {self.event_time} {self.id}"
@@ -283,6 +288,11 @@ class RemoteRealmAuditLog(AbstractRealmAuditLog):
# The remote_id field lets us deduplicate data from the remote server
remote_id = models.IntegerField(null=True)
acting_remote_user = models.ForeignKey(
RemoteRealmBillingUser, null=True, on_delete=models.SET_NULL
)
acting_support_user = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL)
@override
def __str__(self) -> str:
return f"{self.server!r} {self.event_type} {self.event_time} {self.id}"