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
+2 -1
View File
@@ -551,7 +551,8 @@ CREATE TYPE resource_type AS ENUM (
'chat',
'user_secret',
'ai_provider',
'ai_provider_key'
'ai_provider_key',
'group_ai_budget'
);
CREATE TYPE shareable_workspace_owners AS ENUM (
@@ -0,0 +1 @@
-- Postgres does not support removing enum values.
@@ -0,0 +1,2 @@
-- Audit log resource type for group AI budgets.
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'group_ai_budget';
+19
View File
@@ -3,6 +3,7 @@ package database
import (
"database/sql"
"encoding/hex"
"fmt"
"slices"
"sort"
"strconv"
@@ -83,6 +84,24 @@ type AuditableGroup struct {
Members []GroupMemberTable `json:"members"`
}
// AuditableGroupAiBudget is the audit-log representation of GroupAiBudget.
// It enriches the raw record with the group's name and a human-readable
// spend limit so audit entries can display meaningful values instead of
// UUIDs and micros.
type AuditableGroupAiBudget struct {
GroupAiBudget
GroupName string `json:"group_name"`
SpendLimit string `json:"spend_limit"`
}
func (b GroupAiBudget) Auditable(groupName string) AuditableGroupAiBudget {
return AuditableGroupAiBudget{
GroupAiBudget: b,
GroupName: groupName,
SpendLimit: fmt.Sprintf("$%.2f", float64(b.SpendLimitMicros)/1_000_000),
}
}
// Auditable returns an object that can be used in audit logs.
// Covers both group and group member changes.
func (g Group) Auditable(members []GroupMember) AuditableGroup {
+4 -1
View File
@@ -3318,6 +3318,7 @@ const (
ResourceTypeUserSecret ResourceType = "user_secret"
ResourceTypeAiProvider ResourceType = "ai_provider"
ResourceTypeAiProviderKey ResourceType = "ai_provider_key"
ResourceTypeGroupAiBudget ResourceType = "group_ai_budget"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -3387,7 +3388,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeChat,
ResourceTypeUserSecret,
ResourceTypeAiProvider,
ResourceTypeAiProviderKey:
ResourceTypeAiProviderKey,
ResourceTypeGroupAiBudget:
return true
}
return false
@@ -3426,6 +3428,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeUserSecret,
ResourceTypeAiProvider,
ResourceTypeAiProviderKey,
ResourceTypeGroupAiBudget,
}
}