diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 50dc2e4..74362c0 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -2,6 +2,8 @@ package api import ( + "crypto/sha256" + "encoding/hex" "net/http" "strings" "time" @@ -148,6 +150,39 @@ func (s *Server) RequireWritable(next http.Handler) http.Handler { }) } +// RequireRunnerAuth extracts and validates a runner token from the +// Authorization header. On success, injects RunnerIdentity into context +// and updates the runner's heartbeat. +func (s *Server) RequireRunnerAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := extractBearerToken(r) + if token == "" || !strings.HasPrefix(token, "silo_runner_") { + writeError(w, http.StatusUnauthorized, "unauthorized", "Runner token required") + return + } + + hash := sha256.Sum256([]byte(token)) + tokenHash := hex.EncodeToString(hash[:]) + + runner, err := s.jobs.GetRunnerByToken(r.Context(), tokenHash) + if err != nil || runner == nil { + writeError(w, http.StatusUnauthorized, "unauthorized", "Invalid runner token") + return + } + + // Update heartbeat on every authenticated request + _ = s.jobs.Heartbeat(r.Context(), runner.ID) + + identity := &auth.RunnerIdentity{ + ID: runner.ID, + Name: runner.Name, + Tags: runner.Tags, + } + ctx := auth.ContextWithRunner(r.Context(), identity) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + func extractBearerToken(r *http.Request) string { h := r.Header.Get("Authorization") if strings.HasPrefix(h, "Bearer ") { diff --git a/internal/auth/runner.go b/internal/auth/runner.go new file mode 100644 index 0000000..2b7a0c1 --- /dev/null +++ b/internal/auth/runner.go @@ -0,0 +1,24 @@ +package auth + +import "context" + +const runnerContextKey contextKey = iota + 1 + +// RunnerIdentity represents an authenticated runner in the request context. +type RunnerIdentity struct { + ID string + Name string + Tags []string +} + +// RunnerFromContext extracts the authenticated runner from the request context. +// Returns nil if no runner is present. +func RunnerFromContext(ctx context.Context) *RunnerIdentity { + r, _ := ctx.Value(runnerContextKey).(*RunnerIdentity) + return r +} + +// ContextWithRunner returns a new context carrying the given runner identity. +func ContextWithRunner(ctx context.Context, r *RunnerIdentity) context.Context { + return context.WithValue(ctx, runnerContextKey, r) +}