feat: audit user AI budget override mutations (#25745)

Relates to
https://linear.app/codercom/issue/AIGOV-285/add-user-budget-overrides-table-and-crud-api

Adds audit-log support for `user_ai_budget_override` mutations. Without
it, an admin could quietly change a user's per-user spend cap (e.g. from
`$500` to `$50`), reassign it to a different group, or delete it
entirely with no record of who did it.

Both write (`create-or-update`) and delete actions now generate audit
log entries. Unlike group AI budgets, which only track `spend_limit`,
overrides also track `group_name`: an override can be reassigned to a
different attributed group, so that change needs to show up in the diff.
The raw `spend_limit_micros`, IDs, and timestamps are ignored in favor
of the human-readable `spend_limit` and `group_name`.

Depends on #25439.

## Screenshot

<img width="1343" height="514" alt="image"
src="https://github.com/user-attachments/assets/aee30f58-6e81-435e-9bca-5bc98f49d8d3"
/>
This commit is contained in:
Yevhenii Shcherbina
2026-06-10 00:29:06 +00:00
committed by GitHub
parent f95f5e6f86
commit 360611ea15
17 changed files with 429 additions and 37 deletions
+2
View File
@@ -22822,6 +22822,7 @@ const docTemplate = `{
"ai_provider_key",
"ai_gateway_key",
"group_ai_budget",
"user_ai_budget_override",
"chat",
"user_secret",
"user_skill"
@@ -22858,6 +22859,7 @@ const docTemplate = `{
"ResourceTypeAIProviderKey",
"ResourceTypeAIGatewayKey",
"ResourceTypeGroupAIBudget",
"ResourceTypeUserAIBudgetOverride",
"ResourceTypeChat",
"ResourceTypeUserSecret",
"ResourceTypeUserSkill"
+2
View File
@@ -20929,6 +20929,7 @@
"ai_provider_key",
"ai_gateway_key",
"group_ai_budget",
"user_ai_budget_override",
"chat",
"user_secret",
"user_skill"
@@ -20965,6 +20966,7 @@
"ResourceTypeAIProviderKey",
"ResourceTypeAIGatewayKey",
"ResourceTypeGroupAIBudget",
"ResourceTypeUserAIBudgetOverride",
"ResourceTypeChat",
"ResourceTypeUserSecret",
"ResourceTypeUserSkill"
+4
View File
@@ -620,6 +620,10 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
return ""
}
return fmt.Sprintf("/organizations/%s/groups/%s", org.Name, group.Name)
case database.ResourceTypeUserAiBudgetOverride:
// TODO: point at the user's AI budget override management page
// once it ships. Until then, the audit row links nowhere.
return ""
default:
return ""
}
+1
View File
@@ -39,6 +39,7 @@ type Auditable interface {
database.AIGatewayKey |
database.Chat |
database.AuditableGroupAiBudget |
database.AuditableUserAiBudgetOverride |
database.UserSecret |
database.UserSkill
}
+10
View File
@@ -142,6 +142,8 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.Name
case database.AuditableGroupAiBudget:
return typed.GroupName
case database.AuditableUserAiBudgetOverride:
return typed.Username
case database.Chat:
// Chat titles can contain sensitive content (secrets, internal
// project names), so we use a short UUID prefix as a display
@@ -228,6 +230,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.AuditableGroupAiBudget:
return typed.GroupID
case database.AuditableUserAiBudgetOverride:
return typed.UserID
case database.Chat:
return typed.ID
case database.UserSecret:
@@ -299,6 +303,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeAIGatewayKey
case database.AuditableGroupAiBudget:
return database.ResourceTypeGroupAiBudget
case database.AuditableUserAiBudgetOverride:
return database.ResourceTypeUserAiBudgetOverride
case database.Chat:
return database.ResourceTypeChat
case database.UserSecret:
@@ -378,6 +384,10 @@ func ResourceRequiresOrgID[T Auditable]() bool {
case database.AuditableGroupAiBudget:
// Group AI budgets are org-scoped through their parent group.
return true
case database.AuditableUserAiBudgetOverride:
// User AI budget overrides are org-scoped through their
// attributed group.
return true
case database.Chat:
// Chats always have a non-null organization_id (since
// migration 000467).
+2 -1
View File
@@ -569,7 +569,8 @@ CREATE TYPE resource_type AS ENUM (
'ai_provider_key',
'group_ai_budget',
'user_skill',
'ai_gateway_key'
'ai_gateway_key',
'user_ai_budget_override'
);
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 user AI budget overrides.
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'user_ai_budget_override';
+20
View File
@@ -102,6 +102,26 @@ func (b GroupAiBudget) Auditable(groupName string) AuditableGroupAiBudget {
}
}
// AuditableUserAiBudgetOverride is the audit-log representation of
// UserAiBudgetOverride. It enriches the raw record with the username, the
// attributed group's name, and a human-readable spend limit so audit
// entries can display meaningful values instead of UUIDs and micros.
type AuditableUserAiBudgetOverride struct {
UserAiBudgetOverride
Username string `json:"username"`
GroupName string `json:"group_name"`
SpendLimit string `json:"spend_limit"`
}
func (o UserAiBudgetOverride) Auditable(username, groupName string) AuditableUserAiBudgetOverride {
return AuditableUserAiBudgetOverride{
UserAiBudgetOverride: o,
Username: username,
GroupName: groupName,
SpendLimit: fmt.Sprintf("$%.2f", float64(o.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
@@ -3366,6 +3366,7 @@ const (
ResourceTypeGroupAiBudget ResourceType = "group_ai_budget"
ResourceTypeUserSkill ResourceType = "user_skill"
ResourceTypeAIGatewayKey ResourceType = "ai_gateway_key"
ResourceTypeUserAiBudgetOverride ResourceType = "user_ai_budget_override"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -3438,7 +3439,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeAIProviderKey,
ResourceTypeGroupAiBudget,
ResourceTypeUserSkill,
ResourceTypeAIGatewayKey:
ResourceTypeAIGatewayKey,
ResourceTypeUserAiBudgetOverride:
return true
}
return false
@@ -3480,6 +3482,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeGroupAiBudget,
ResourceTypeUserSkill,
ResourceTypeAIGatewayKey,
ResourceTypeUserAiBudgetOverride,
}
}