feat: audit group AI budget mutations (#25374)

Relates to
https://linear.app/codercom/issue/AIGOV-284/add-group-budgets-table-and-crud-api

Adds audit-log support for `group_ai_budget` mutations. Without it, an
admin could silently lower a spend limit from `$500` to `$50` or delete
a budget entirely, with no record of who performed the action.

Both write (`create-or-update`) and delete actions now produce audit log
entries, including before/after diffs for `spend_limit_micros`.

Depends on #25203.

## Old Version
<img width="1340" height="456" alt="image"
src="https://github.com/user-attachments/assets/e9ff52fb-a905-4aef-a4ee-7cdc58e68b75"
/>

## New Version (see
https://github.com/coder/coder/pull/25374/changes/9d22833de87cc106c24142c1d471a3f71872bf67)
<img width="1347" height="496" alt="image"
src="https://github.com/user-attachments/assets/1b9bbfa1-f86d-48e3-a0b1-266eb76f851f"
/>
This commit is contained in:
Yevhenii Shcherbina
2026-05-18 15:17:20 -04:00
committed by GitHub
parent 385146000b
commit 2732378da2
17 changed files with 223 additions and 27 deletions
+1
View File
@@ -37,6 +37,7 @@ type Auditable interface {
database.AIProvider |
database.AIProviderKey |
database.Chat |
database.AuditableGroupAiBudget |
database.UserSecret
}
+9
View File
@@ -141,6 +141,8 @@ func ResourceTarget[T Auditable](tgt T) string {
// provider's UUID so the row can be correlated back to its
// provider in the audit UI.
return typed.ProviderID.String()
case database.AuditableGroupAiBudget:
return typed.GroupName
case database.Chat:
// Chat titles can contain sensitive content (secrets, internal
// project names), so we use a short UUID prefix as a display
@@ -221,6 +223,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.AIProviderKey:
return typed.ID
case database.AuditableGroupAiBudget:
return typed.GroupID
case database.Chat:
return typed.ID
case database.UserSecret:
@@ -286,6 +290,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeAiProvider
case database.AIProviderKey:
return database.ResourceTypeAiProviderKey
case database.AuditableGroupAiBudget:
return database.ResourceTypeGroupAiBudget
case database.Chat:
return database.ResourceTypeChat
case database.UserSecret:
@@ -356,6 +362,9 @@ func ResourceRequiresOrgID[T Auditable]() bool {
case database.AIProviderKey:
// AI provider keys are deployment-scoped, not org-scoped.
return false
case database.AuditableGroupAiBudget:
// Group AI budgets are org-scoped through their parent group.
return true
case database.Chat:
// Chats always have a non-null organization_id (since
// migration 000467).