mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	remote_billing: Store acting users in remote user audit logs.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							a13e42f18a
						
					
				
				
					commit
					651590c49a
				
			@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
            )
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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}"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user