1
0
forked from wrenn/wrenn

Add audit log infrastructure and GET /v1/audit-logs endpoint

Introduces an append-only audit trail for all user and system actions:
sandbox lifecycle (create/pause/resume/destroy/auto-pause), snapshots,
team rename, API key create/revoke, member add/remove/leave/role_update,
and BYOC host add/delete/marked_down/marked_up.

- New audit_logs table (migration) with team_id, actor, resource,
  action, scope (team|admin), status (success|info|warning|error),
  metadata, and created_at
- AuditLogger (internal/audit) with named fire-and-forget methods per
  event; system actor used for background events (HostMonitor, TTL reaper)
- GET /v1/audit-logs: JWT-only, cursor pagination (max 200), multi-value
  filters for resource_type and action (comma-sep or repeated params);
  members see team-scoped events only, admins/owners see all
- AuthContext extended with APIKeyID + APIKeyName so API key requests
  record meaningful actor identity
- HostMonitor wired with AuditLogger for auto-pause and host marked_down
This commit is contained in:
2026-03-25 05:15:16 +06:00
parent 9878156798
commit 1be30034bd
21 changed files with 938 additions and 43 deletions

111
internal/db/audit.sql.go Normal file
View File

@ -0,0 +1,111 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: audit.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const insertAuditLog = `-- name: InsertAuditLog :exec
INSERT INTO audit_logs (id, team_id, actor_type, actor_id, actor_name, resource_type, resource_id, action, scope, status, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type InsertAuditLogParams struct {
ID string `json:"id"`
TeamID string `json:"team_id"`
ActorType string `json:"actor_type"`
ActorID pgtype.Text `json:"actor_id"`
ActorName pgtype.Text `json:"actor_name"`
ResourceType string `json:"resource_type"`
ResourceID pgtype.Text `json:"resource_id"`
Action string `json:"action"`
Scope string `json:"scope"`
Status string `json:"status"`
Metadata []byte `json:"metadata"`
}
func (q *Queries) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error {
_, err := q.db.Exec(ctx, insertAuditLog,
arg.ID,
arg.TeamID,
arg.ActorType,
arg.ActorID,
arg.ActorName,
arg.ResourceType,
arg.ResourceID,
arg.Action,
arg.Scope,
arg.Status,
arg.Metadata,
)
return err
}
const listAuditLogs = `-- name: ListAuditLogs :many
SELECT id, team_id, actor_type, actor_id, actor_name, resource_type, resource_id, action, scope, status, metadata, created_at FROM audit_logs
WHERE team_id = $1
AND scope = ANY($2::text[])
AND (cardinality($3::text[]) = 0 OR resource_type = ANY($3::text[]))
AND (cardinality($4::text[]) = 0 OR action = ANY($4::text[]))
AND ($5::timestamptz IS NULL OR created_at < $5
OR (created_at = $5 AND id < $6))
ORDER BY created_at DESC, id DESC
LIMIT $7
`
type ListAuditLogsParams struct {
TeamID string `json:"team_id"`
Column2 []string `json:"column_2"`
Column3 []string `json:"column_3"`
Column4 []string `json:"column_4"`
Column5 pgtype.Timestamptz `json:"column_5"`
ID string `json:"id"`
Limit int32 `json:"limit"`
}
func (q *Queries) ListAuditLogs(ctx context.Context, arg ListAuditLogsParams) ([]AuditLog, error) {
rows, err := q.db.Query(ctx, listAuditLogs,
arg.TeamID,
arg.Column2,
arg.Column3,
arg.Column4,
arg.Column5,
arg.ID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AuditLog
for rows.Next() {
var i AuditLog
if err := rows.Scan(
&i.ID,
&i.TeamID,
&i.ActorType,
&i.ActorID,
&i.ActorName,
&i.ResourceType,
&i.ResourceID,
&i.Action,
&i.Scope,
&i.Status,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}