feat: audit ai config changes

This commit is contained in:
QiuSW
2026-07-02 11:42:39 +08:00
parent 18ac054dc5
commit c0020e8d29
16 changed files with 520 additions and 23 deletions
+137 -2
View File
@@ -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))