feat: audit ai config changes
This commit is contained in:
+137
-2
@@ -2,12 +2,13 @@ import base64
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
|
||||
|
||||
from apps.ai.admin import AiModelAdmin
|
||||
from apps.ai.admin import AiConfigAuditLogAdmin, AiModelAdmin, ModelAliasAdmin
|
||||
from apps.ai.aliases import AliasNotFoundError, ModelCapabilityError, resolve_alias
|
||||
from apps.ai.importers import import_ai_models_config
|
||||
from apps.ai.models import AiModel, ModelAlias
|
||||
from apps.ai.models import AiConfigAuditLog, AiModel, ModelAlias
|
||||
from apps.ai.providers import AiCapabilityError, ResolvedModel, get_provider, resolve_api_type
|
||||
from apps.ai.providers.openai_compatible import ChatCompletionsProvider, ImagesEditsProvider
|
||||
|
||||
@@ -425,3 +426,137 @@ class AiModelAdminTests(TestCase):
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn("AI_KEY_ENCRYPTION_KEY is not configured", str(form.errors))
|
||||
|
||||
|
||||
@override_settings(AI_KEY_ENCRYPTION_KEY=TEST_ENCRYPTION_KEY)
|
||||
class AiConfigAuditAdminTests(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.request = RequestFactory().post("/admin/")
|
||||
self.request.user = get_user_model().objects.create_superuser(
|
||||
username="auditor",
|
||||
email="auditor@example.com",
|
||||
password="password",
|
||||
)
|
||||
|
||||
def create_ai_model(
|
||||
self,
|
||||
*,
|
||||
name="GPT-5.5 text",
|
||||
model="gpt-5.5",
|
||||
capabilities=None,
|
||||
api_type=AiModel.ApiType.CHAT,
|
||||
url="https://api.vectorengine.ai/v1",
|
||||
api_key="sk-test-secret",
|
||||
):
|
||||
ai_model = AiModel(
|
||||
name=name,
|
||||
url=url,
|
||||
model=model,
|
||||
api_type=api_type,
|
||||
capabilities=capabilities or ["text"],
|
||||
)
|
||||
ai_model.set_api_key(api_key)
|
||||
ai_model.save()
|
||||
return ai_model
|
||||
|
||||
def test_aimodel_admin_create_writes_sanitized_audit_log(self):
|
||||
ai_model = AiModel(
|
||||
name="GPT-5.5 text",
|
||||
url="https://api.vectorengine.ai/v1",
|
||||
model="gpt-5.5",
|
||||
api_type=AiModel.ApiType.CHAT,
|
||||
capabilities=["text"],
|
||||
)
|
||||
ai_model.set_api_key("sk-created-secret")
|
||||
AiModelAdmin(AiModel, self.site).save_model(
|
||||
self.request,
|
||||
ai_model,
|
||||
form=None,
|
||||
change=False,
|
||||
)
|
||||
|
||||
log = AiConfigAuditLog.objects.get()
|
||||
self.assertEqual(log.actor, self.request.user)
|
||||
self.assertEqual(log.action, AiConfigAuditLog.Action.CREATE)
|
||||
self.assertEqual(log.target_type, AiConfigAuditLog.TargetType.AI_MODEL)
|
||||
self.assertIn("api_key", log.changed_fields)
|
||||
self.assertEqual(log.changes["api_key"], {"old": "empty", "new": "set"})
|
||||
self.assertNotIn("sk-created-secret", str(log.changes))
|
||||
self.assertNotIn("fernet:", str(log.changes))
|
||||
|
||||
def test_aimodel_admin_update_logs_field_and_key_changes(self):
|
||||
ai_model = self.create_ai_model()
|
||||
ai_model.url = "https://api.vectorengine.ai/v2"
|
||||
ai_model.set_api_key("sk-new-secret")
|
||||
|
||||
AiModelAdmin(AiModel, self.site).save_model(
|
||||
self.request,
|
||||
ai_model,
|
||||
form=None,
|
||||
change=True,
|
||||
)
|
||||
|
||||
log = AiConfigAuditLog.objects.get()
|
||||
self.assertEqual(log.action, AiConfigAuditLog.Action.UPDATE)
|
||||
self.assertEqual(set(log.changed_fields), {"url", "api_key"})
|
||||
self.assertEqual(
|
||||
log.changes["url"],
|
||||
{
|
||||
"old": "https://api.vectorengine.ai/v1",
|
||||
"new": "https://api.vectorengine.ai/v2",
|
||||
},
|
||||
)
|
||||
self.assertEqual(log.changes["api_key"], {"old": "set", "new": "set"})
|
||||
self.assertNotIn("sk-new-secret", str(log.changes))
|
||||
|
||||
def test_model_alias_admin_update_logs_mapping_change(self):
|
||||
text_model = self.create_ai_model(name="Text model", model="gpt-5.5")
|
||||
image_model = self.create_ai_model(
|
||||
name="Image model",
|
||||
model="gpt-image-2",
|
||||
capabilities=["image"],
|
||||
api_type=AiModel.ApiType.IMAGES_EDITS,
|
||||
url="https://api.vectorengine.ai/v1/images/edits",
|
||||
)
|
||||
alias = ModelAlias.objects.create(
|
||||
operation_type=ModelAlias.OperationType.IMAGE,
|
||||
alias="image-standard",
|
||||
ai_model=text_model,
|
||||
)
|
||||
alias.ai_model = image_model
|
||||
|
||||
ModelAliasAdmin(ModelAlias, self.site).save_model(
|
||||
self.request,
|
||||
alias,
|
||||
form=None,
|
||||
change=True,
|
||||
)
|
||||
|
||||
log = AiConfigAuditLog.objects.get()
|
||||
self.assertEqual(log.target_type, AiConfigAuditLog.TargetType.MODEL_ALIAS)
|
||||
self.assertEqual(log.changed_fields, ["ai_model"])
|
||||
self.assertEqual(
|
||||
log.changes["ai_model"],
|
||||
{"old": text_model.id, "new": image_model.id},
|
||||
)
|
||||
|
||||
def test_aimodel_admin_delete_writes_audit_log(self):
|
||||
ai_model = self.create_ai_model()
|
||||
target_id = ai_model.id
|
||||
|
||||
AiModelAdmin(AiModel, self.site).delete_model(self.request, ai_model)
|
||||
|
||||
log = AiConfigAuditLog.objects.get()
|
||||
self.assertEqual(log.action, AiConfigAuditLog.Action.DELETE)
|
||||
self.assertEqual(log.target_id, target_id)
|
||||
self.assertIn("api_key", log.changed_fields)
|
||||
self.assertEqual(log.changes["api_key"], {"old": "set", "new": "empty"})
|
||||
|
||||
def test_audit_log_admin_is_read_only(self):
|
||||
model_admin = AiConfigAuditLogAdmin(AiConfigAuditLog, self.site)
|
||||
|
||||
self.assertFalse(model_admin.has_add_permission(self.request))
|
||||
self.assertFalse(model_admin.has_change_permission(self.request))
|
||||
self.assertFalse(model_admin.has_delete_permission(self.request))
|
||||
self.assertIn("changes", model_admin.get_readonly_fields(self.request))
|
||||
|
||||
Reference in New Issue
Block a user