1
0
forked from wrenn/wrenn

feat: add audit logging for all admin actions and admin audit page

Log every admin-panel action (user activate/deactivate, team BYOC toggle,
team delete, template delete, build create/cancel) to the audit_logs table
under PlatformTeamID with scope "admin".

Add GET /v1/admin/audit-logs endpoint and /admin/audit frontend page with
infinite scroll and hierarchical filters. Expose audit.Entry + Log() for
cloud repo extensibility.

Fix seed_platform_team down-migration FK violation by deleting dependent
rows before the team row.
This commit is contained in:
2026-04-21 15:41:45 +06:00
parent edec170652
commit 7fd801c1eb
10 changed files with 917 additions and 51 deletions

View File

@ -82,6 +82,53 @@ func marshalMeta(meta map[string]any) []byte {
return b
}
// Entry describes a single audit log event. Extensions (e.g. the cloud repo)
// use this with AuditLogger.Log to record custom events without modifying the
// OSS typed methods.
type Entry struct {
TeamID pgtype.UUID
ActorType string // "user", "api_key", "system"
ActorID string // prefixed ID string; empty for system
ActorName string // human-readable; empty for system
ResourceType string
ResourceID string // prefixed ID or name; empty when not applicable
Action string
Scope string // "team" or "admin"
Status string // "success", "info", "warning", "error"
Metadata map[string]any
}
// Log writes a custom audit log entry. This is the extension point for the
// cloud repo to record events with resource types and actions not covered by
// the typed helpers (LogSandboxCreate, etc.). Fire-and-forget like all other
// audit methods.
func (l *AuditLogger) Log(ctx context.Context, e Entry) {
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: e.TeamID,
ActorType: e.ActorType,
ActorID: optText(e.ActorID),
ActorName: e.ActorName,
ResourceType: e.ResourceType,
ResourceID: optText(e.ResourceID),
Action: e.Action,
Scope: e.Scope,
Status: e.Status,
Metadata: MarshalMeta(e.Metadata),
})
}
// ActorFromContext extracts actor fields from an auth.AuthContext for use in
// custom audit entries. Returns actor_type, actor_id, and actor_name.
func ActorFromContext(ac auth.AuthContext) (actorType, actorID, actorName string) {
return actorFields(ac)
}
// MarshalMeta serializes metadata to JSON bytes. Returns "{}" for nil/empty maps.
func MarshalMeta(meta map[string]any) []byte {
return marshalMeta(meta)
}
// optText returns a valid pgtype.Text if s is non-empty, otherwise an invalid (NULL) one.
func optText(s string) pgtype.Text {
if s == "" {
@ -507,6 +554,133 @@ func (l *AuditLogger) LogHostDelete(ctx context.Context, ac auth.AuthContext, ho
})
}
// --- User events (scope: admin) ---
func (l *AuditLogger) LogUserActivate(ctx context.Context, ac auth.AuthContext, userID pgtype.UUID, email string) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "user",
ResourceID: optText(id.FormatUserID(userID)),
Action: "activate",
Scope: "admin",
Status: "success",
Metadata: marshalMeta(map[string]any{"email": email}),
})
}
func (l *AuditLogger) LogUserDeactivate(ctx context.Context, ac auth.AuthContext, userID pgtype.UUID, email string) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "user",
ResourceID: optText(id.FormatUserID(userID)),
Action: "deactivate",
Scope: "admin",
Status: "warning",
Metadata: marshalMeta(map[string]any{"email": email}),
})
}
// --- Team admin events (scope: admin) ---
func (l *AuditLogger) LogTeamSetBYOC(ctx context.Context, ac auth.AuthContext, teamID pgtype.UUID, enabled bool) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "team",
ResourceID: optText(id.FormatTeamID(teamID)),
Action: "set_byoc",
Scope: "admin",
Status: "info",
Metadata: marshalMeta(map[string]any{"enabled": enabled}),
})
}
func (l *AuditLogger) LogTeamDelete(ctx context.Context, ac auth.AuthContext, teamID pgtype.UUID) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "team",
ResourceID: optText(id.FormatTeamID(teamID)),
Action: "delete",
Scope: "admin",
Status: "warning",
Metadata: []byte("{}"),
})
}
// --- Template events (scope: admin) ---
func (l *AuditLogger) LogTemplateDelete(ctx context.Context, ac auth.AuthContext, name string) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "template",
ResourceID: optText(name),
Action: "delete",
Scope: "admin",
Status: "warning",
Metadata: []byte("{}"),
})
}
// --- Build events (scope: admin) ---
func (l *AuditLogger) LogBuildCreate(ctx context.Context, ac auth.AuthContext, buildID pgtype.UUID, name string) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "build",
ResourceID: optText(id.FormatBuildID(buildID)),
Action: "create",
Scope: "admin",
Status: "success",
Metadata: marshalMeta(map[string]any{"name": name}),
})
}
func (l *AuditLogger) LogBuildCancel(ctx context.Context, ac auth.AuthContext, buildID pgtype.UUID) {
actorType, actorID, actorName := actorFields(ac)
l.write(ctx, db.InsertAuditLogParams{
ID: id.NewAuditLogID(),
TeamID: id.PlatformTeamID,
ActorType: actorType,
ActorID: optText(actorID),
ActorName: actorName,
ResourceType: "build",
ResourceID: optText(id.FormatBuildID(buildID)),
Action: "cancel",
Scope: "admin",
Status: "warning",
Metadata: []byte("{}"),
})
}
// LogHostMarkedDown records a system-initiated host status transition to unreachable.
// Scoped to "team" so BYOC team members can see when their hosts go down.
func (l *AuditLogger) LogHostMarkedDown(ctx context.Context, teamID, hostID pgtype.UUID) {