mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
Generated
+2
@@ -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"
|
||||
|
||||
Generated
+2
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ type Auditable interface {
|
||||
database.AIGatewayKey |
|
||||
database.Chat |
|
||||
database.AuditableGroupAiBudget |
|
||||
database.AuditableUserAiBudgetOverride |
|
||||
database.UserSecret |
|
||||
database.UserSkill
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Generated
+2
-1
@@ -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';
|
||||
@@ -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 {
|
||||
|
||||
Generated
+4
-1
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user