Files
warmbly/internal/api/handler/admin_releases.go
T
Matthew Meszaros 126114b34f feat(admin): audit-log every mutating worker / credentials / release action
The new admin endpoints for SSH worker management, AWS credentials,
worker profiles, releases, and system operations bypassed the existing
audit-logging pattern. Now every mutating action records a row in the
audit log with adminID, IP, user-agent, and operation-specific metadata
(never secret values — for credential updates we record which fields
were rotated, not what they became).

New action constants:
  test, install, restart, upgrade, uninstall, rotate_keys, apply,
  assign, system_update, reboot, check_releases

New entity types:
  worker, aws_credentials, worker_profile, release

Read-only endpoints (list/get/status/logs) intentionally not audited —
they don't change state and would flood the log.

Each handler calls a small h.audit(c, action, entity, &id, metadata)
helper that pulls adminID from the admin middleware and fires the
existing AuditService.LogAction (fire-and-forget, never blocks the
response).
2026-05-18 14:55:44 +00:00

101 lines
3.2 KiB
Go

// Release-handling endpoints.
//
// POST /webhooks/github/releases — public, HMAC-validated. GitHub posts here
// when a release event fires.
// POST /admin/releases/check — manual trigger from the dashboard.
// GET /admin/releases/state — last known per-channel resolution.
// PUT /admin/worker-profiles/:id/release — set release channel + auto-update.
//
// The webhook endpoint is intentionally not behind admin auth; security comes
// from the X-Hub-Signature-256 HMAC. If RELEASES_WEBHOOK_SECRET isn't set,
// the endpoint refuses all requests.
package handler
import (
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/warmbly/warmbly/internal/app/releases"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
)
func (h *Handler) GithubReleasesWebhook(c *gin.Context) {
if h.ReleasesService == nil {
c.JSON(http.StatusNotImplemented, gin.H{"error": "releases service not configured"})
return
}
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "read body: " + err.Error()})
return
}
signature := c.GetHeader("X-Hub-Signature-256")
eventType := c.GetHeader("X-GitHub-Event")
if err := h.ReleasesService.HandleWebhook(c.Request.Context(), body, signature, eventType); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *Handler) AdminCheckReleases(c *gin.Context) {
if h.ReleasesService == nil {
errx.JSON(c, errx.New(errx.NotFound, "releases service not configured"))
return
}
changed, err := h.ReleasesService.CheckGitHub(c.Request.Context())
if err != nil {
errx.JSON(c, errx.New(errx.Internal, err.Error()))
return
}
h.audit(c, models.AuditActionCheckReleases, models.AuditEntityRelease, nil, map[string]string{
"changed_profiles": fmt.Sprintf("%d", len(changed)),
})
c.JSON(http.StatusOK, gin.H{
"state": h.ReleasesService.GetState(),
"changed": changed,
})
}
func (h *Handler) AdminReleasesState(c *gin.Context) {
if h.ReleasesService == nil {
c.JSON(http.StatusOK, gin.H{"enabled": false})
return
}
c.JSON(http.StatusOK, h.ReleasesService.GetState())
}
type setReleaseBody struct {
Channel string `json:"channel" binding:"required,oneof=pinned stable dev"`
AutoUpdate bool `json:"auto_update"`
}
func (h *Handler) AdminSetProfileRelease(c *gin.Context) {
id, ok := parseUUID(c, "id")
if !ok {
return
}
var body setReleaseBody
if err := c.ShouldBindJSON(&body); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
if err := h.CredentialsRepo.UpdateProfileRelease(c.Request.Context(), id, models.ReleaseChannel(body.Channel), body.AutoUpdate); err != nil {
errx.JSON(c, errx.New(errx.Internal, err.Error()))
return
}
h.audit(c, models.AuditActionUpdate, models.AuditEntityWorkerProfile, &id, map[string]string{
"release_channel": body.Channel,
"auto_update": fmt.Sprintf("%t", body.AutoUpdate),
})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// Compile-time check that the releases package is referenced (keeps the
// import stable across edits even if every method body changes).
var _ = (*releases.Service)(nil)