feat: audit user secret create, update, and delete (#24756)

Emit user secret audit log entries for create/update/delete operations.
Reads stay un-audited, matching every other resource.

Audit log entries record changes in user secret name, environment
variable name, file path, and value. The secret value column is marked
`ActionSecret` so the diff records the change without showing the
ciphertext or plaintext.

Closes a TOCTOU window on delete to ensure no phantom audit logs for a
delete of a non-existent secret. Secret update accepts a small TOCTOU
window matching the other audited resources (templates, workspaces,
chats). The two-query pattern is wrapped in a transaction so audit state
can't leak from a failed mutation.
This commit is contained in:
Zach
2026-04-29 12:57:47 -06:00
committed by GitHub
parent 25ae415481
commit 1c30d52b2b
24 changed files with 555 additions and 48 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ type Auditable interface {
idpsync.RoleSyncSettings |
database.TaskTable |
database.AiSeatState |
database.Chat
database.Chat |
database.UserSecret
}
// Map is a map of changed fields in an audited resource. It maps field names to
+9
View File
@@ -142,6 +142,8 @@ func ResourceTarget[T Auditable](tgt T) string {
// for display; collisions affect the display label and search
// filter but not the primary resource identifier.
return typed.ID.String()[:8]
case database.UserSecret:
return typed.Name
default:
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
}
@@ -210,6 +212,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.UserID
case database.Chat:
return typed.ID
case database.UserSecret:
return typed.ID
default:
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
}
@@ -269,6 +273,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeAiSeat
case database.Chat:
return database.ResourceTypeChat
case database.UserSecret:
return database.ResourceTypeUserSecret
default:
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
}
@@ -333,6 +339,9 @@ func ResourceRequiresOrgID[T Auditable]() bool {
// Chats always have a non-null organization_id (since
// migration 000467).
return true
case database.UserSecret:
// User secrets are global to the user across organizations.
return false
default:
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
}