Skip to content

Commit 2013e30

Browse files
committed
Create project audit logs when an organization is deleted
1 parent ef254bc commit 2013e30

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

readthedocs/core/tasks.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def delete_object(
102102
from simple_history.models import HistoricalRecords
103103

104104
from readthedocs.audit.models import AuditLog
105+
from readthedocs.organizations.models import Organization
105106
from readthedocs.projects.models import Project
106107

107108
task_log = log.bind(model_name=model_name, object_pk=pk, user_id=user_id)
@@ -127,15 +128,24 @@ def delete_object(
127128
if browser:
128129
HistoricalRecords.context.browser = browser
129130

131+
# Collect projects to audit log before deletion.
132+
# When an Organization is deleted, its projects are cascade-deleted
133+
# via signal, so we need to capture them here.
134+
projects_to_log = []
130135
if isinstance(obj, Project):
131-
# Create an audit log entry for each project admin
132-
# so that all of them can see the deletion in their
133-
# personal security log, not just the user who deleted it.
134-
for admin in obj.users.all():
136+
projects_to_log = [obj]
137+
elif isinstance(obj, Organization):
138+
projects_to_log = list(obj.projects.all())
139+
140+
# Create an audit log entry for each project admin
141+
# so that all of them can see the deletion in their
142+
# personal security log, not just the user who deleted it.
143+
for project in projects_to_log:
144+
for admin in project.users.all():
135145
AuditLog.objects.create(
136146
user=admin,
137147
action=AuditLog.PROJECT_DELETE,
138-
project=obj,
148+
project=project,
139149
ip=ip,
140150
browser=browser,
141151
data={"deleted_by": user.username} if user else None,

readthedocs/rtd_tests/tests/test_project_views.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,46 @@ def test_delete(self):
844844
self.assertEqual(self.project.webhook_notifications.all().count(), 0)
845845

846846

847+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
848+
class TestOrganizationDeleteAuditLog(TestCase):
849+
"""Test that deleting an organization creates audit logs for its projects."""
850+
851+
def setUp(self):
852+
self.user = new(User, username="org-owner")
853+
self.user.set_password("test")
854+
self.user.save()
855+
self.client.login(username="org-owner", password="test")
856+
857+
self.project = get(Project, slug="org-project", users=[self.user])
858+
self.organization = get(
859+
Organization,
860+
owners=[self.user],
861+
projects=[self.project],
862+
)
863+
864+
def test_delete_organization_creates_audit_log_for_projects(self):
865+
from readthedocs.audit.models import AuditLog
866+
867+
project_slug = self.project.slug
868+
with mock.patch(
869+
"readthedocs.projects.tasks.utils.clean_project_resources"
870+
):
871+
response = self.client.post(
872+
reverse("organization_delete", args=[self.organization.slug]),
873+
)
874+
self.assertEqual(response.status_code, 302)
875+
876+
self.assertFalse(Project.objects.filter(slug=project_slug).exists())
877+
self.assertFalse(Organization.objects.filter(pk=self.organization.pk).exists())
878+
879+
logs = AuditLog.objects.filter(action=AuditLog.PROJECT_DELETE)
880+
self.assertEqual(logs.count(), 1)
881+
log_entry = logs.first()
882+
self.assertEqual(log_entry.log_project_slug, project_slug)
883+
self.assertEqual(log_entry.log_user_username, self.user.username)
884+
self.assertEqual(log_entry.data, {"deleted_by": self.user.username})
885+
886+
847887
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
848888
class TestWebhooksViewsWithOrganizations(TestWebhooksViews):
849889
def setUp(self):

0 commit comments

Comments
 (0)