Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/audit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@
"Phased rollout created for feature: %s by release pipeline: %s (stage: %s)"
)
PHASED_ROLLOUT_STATE_UPDATED_MESSAGE = "Phased rollout split changed from '%s%%' to '%s%%' for feature '%s' by release pipeline '%s' (stage: '%s')"
PROJECT_CREATED_MESSAGE = "New Project created: %s"
PROJECT_DELETED_MESSAGE = "Project deleted: %s"
1 change: 1 addition & 0 deletions api/audit/related_object_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ class RelatedObjectType(enum.Enum):
WAREHOUSE_CONNECTION = "Warehouse connection"
EXPERIMENT = "Experiment"
METRIC = "Metric"
PROJECT = "project"
32 changes: 32 additions & 0 deletions api/projects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
VIEW_PROJECT,
)
from django.conf import settings
from django.db import transaction
from django.utils.decorators import method_decorator
from drf_spectacular.utils import extend_schema
from rest_framework import status, viewsets
Expand All @@ -16,6 +17,9 @@
from rest_framework.request import Request
from rest_framework.response import Response

from audit.constants import PROJECT_CREATED_MESSAGE, PROJECT_DELETED_MESSAGE
from audit.models import AuditLog
from audit.related_object_type import RelatedObjectType
from environments.dynamodb.migrator import IdentityMigrator
from environments.identities.models import Identity
from environments.serializers import EnvironmentSerializerLight
Expand Down Expand Up @@ -106,10 +110,38 @@ def get_queryset(self): # type: ignore[no-untyped-def]

def perform_create(self, serializer): # type: ignore[no-untyped-def]
project = serializer.save()
is_master_api_key_user = getattr(
self.request.user, "is_master_api_key_user", False
)
if getattr(self.request.user, "is_master_api_key_user", False) is False:
UserProjectPermission.objects.create( # type: ignore[misc]
user=self.request.user, project=project, admin=True
)
AuditLog.objects.create(
project=project,
author=None if is_master_api_key_user else self.request.user,
master_api_key=self.request.user.key if is_master_api_key_user else None,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
related_object_id=project.id,
related_object_type=RelatedObjectType.PROJECT.name,
log=PROJECT_CREATED_MESSAGE % project.name,
)

def perform_destroy(self, instance):
with transaction.atomic():
is_master_api_key_user = getattr(
self.request.user, "is_master_api_key_user", False
)
AuditLog.objects.create(
project=None,
author=None if is_master_api_key_user else self.request.user,
master_api_key=self.request.user.key
if is_master_api_key_user
else None,
related_object_id=instance.id,
related_object_type=RelatedObjectType.PROJECT.name,
log=PROJECT_DELETED_MESSAGE % instance.name,
)
instance.delete()

@action(
detail=False,
Expand Down
44 changes: 44 additions & 0 deletions api/tests/unit/projects/test_unit_projects_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from rest_framework.test import APIClient
from task_processor.task_run_method import TaskRunMethod

from audit.constants import PROJECT_CREATED_MESSAGE, PROJECT_DELETED_MESSAGE
from audit.models import AuditLog
from audit.related_object_type import RelatedObjectType
from environments.dynamodb.types import ProjectIdentityMigrationStatus
from environments.identities.models import Identity
from features.models import Feature, FeatureSegment
Expand Down Expand Up @@ -1069,3 +1072,44 @@ def test_list_projects__default_enforce_feature_owners__returns_false(
assert len(response.json()) > 0
assert "enforce_feature_owners" in response.json()[0]
assert response.json()[0]["enforce_feature_owners"] is False


def test_create_project_creates_audit_log(admin_client, organisation):
# Given
url = reverse("api-v1:projects:project-list")
project_name = "New Audit Log Project"
data = {"name": project_name, "organisation": organisation.id}
initial_audit_log_count = AuditLog.objects.count()

# When
response = admin_client.post(url, data=data)

# Then
assert response.status_code == status.HTTP_201_CREATED
assert AuditLog.objects.count() == initial_audit_log_count + 1

# Verify the audit log details
audit_log = AuditLog.objects.order_by("-created_date").first()
assert audit_log.related_object_type == RelatedObjectType.PROJECT.name
assert audit_log.log == PROJECT_CREATED_MESSAGE % project_name
assert audit_log.project_id == response.data["id"]


def test_delete_project_creates_audit_log(admin_client, project, organisation):
# Given
url = reverse("api-v1:projects:project-detail", args=[project.id])
project_name = project.name
initial_audit_log_count = AuditLog.objects.count()

# When
response = admin_client.delete(url)

# Then
assert response.status_code == status.HTTP_204_NO_CONTENT
assert AuditLog.objects.count() == initial_audit_log_count + 1

# Verify the audit log details
audit_log = AuditLog.objects.order_by("-created_date").first()
assert audit_log.related_object_type == RelatedObjectType.PROJECT.name
assert audit_log.log == PROJECT_DELETED_MESSAGE % project_name
Comment thread
srijantrpth marked this conversation as resolved.
assert audit_log.related_object_id == project.id
Loading