All checks were successful
release-tag / release-image (push) Successful in 2m43s
337 lines
11 KiB
Go
337 lines
11 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/local/glpi-neural-brain/internal/model"
|
|
"github.com/local/glpi-neural-brain/internal/sourceagent"
|
|
)
|
|
|
|
func (s *Server) handleGetControllerPolicy(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
p, err := s.SourceAgents.ControllerPolicy(ctx)
|
|
if err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, p)
|
|
}
|
|
func (s *Server) handleSetControllerPolicy(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
var p sourceagent.ControllerPolicy
|
|
if err := decode(r, &p); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
out, err := s.SourceAgents.SetControllerPolicy(ctx, p)
|
|
if err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if s.Broker != nil {
|
|
s.Broker.Publish(model.Activity{Type: "controller.policy.updated", Source: "brain", Phase: "controller-policy", Message: "Docker-Controller-Policy wurde geändert", Strength: .88, Metadata: map[string]any{"enabled": out.Enabled, "autonomous_enabled": out.AutonomousEnabled, "dry_run": out.DryRun, "allow_destructive": out.AllowDestructive, "max_concurrent_jobs": out.MaxConcurrentJobs}})
|
|
}
|
|
writeJSON(w, 200, out)
|
|
}
|
|
func (s *Server) handleListControllerProfiles(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
p, err := s.SourceAgents.ListControllerProfiles(ctx)
|
|
if err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if p == nil {
|
|
p = []sourceagent.ControllerProfile{}
|
|
}
|
|
writeJSON(w, 200, map[string]any{"profiles": p})
|
|
}
|
|
func (s *Server) handleCreateControllerProfile(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
var p sourceagent.ControllerProfile
|
|
if err := decode(r, &p); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
out, err := s.SourceAgents.UpsertControllerProfile(ctx, p)
|
|
if err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, out)
|
|
}
|
|
func (s *Server) handleUpdateControllerProfile(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
var p sourceagent.ControllerProfile
|
|
if err := decode(r, &p); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
p.ID = r.PathValue("id")
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
out, err := s.SourceAgents.UpsertControllerProfile(ctx, p)
|
|
if err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, out)
|
|
}
|
|
func (s *Server) handleDeleteControllerProfile(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
if err := s.SourceAgents.DeleteControllerProfile(ctx, r.PathValue("id")); err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
func (s *Server) handleRunControllerProfile(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
var in struct {
|
|
DryRun *bool `json:"dry_run"`
|
|
Overrides map[string]any `json:"overrides"`
|
|
}
|
|
_ = decode(r, &in)
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
profile, err := s.SourceAgents.ControllerProfile(ctx, r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, 404, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
params := map[string]any{}
|
|
for k, v := range profile.Config {
|
|
params[k] = v
|
|
}
|
|
for k, v := range in.Overrides {
|
|
params[k] = v
|
|
}
|
|
job := sourceagent.ControllerJob{AgentID: profile.AgentID, ProfileID: profile.ID, Kind: profile.Kind, Purpose: profile.Purpose, Parameters: params}
|
|
if in.DryRun != nil {
|
|
job.DryRun = *in.DryRun
|
|
}
|
|
job, err = s.SourceAgents.QueueControllerJob(ctx, job)
|
|
if err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusAccepted, job)
|
|
}
|
|
func (s *Server) handleListControllerJobs(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
limit := 100
|
|
if n, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && n > 0 {
|
|
limit = n
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
jobs, err := s.SourceAgents.ListControllerJobs(ctx, limit)
|
|
if err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if jobs == nil {
|
|
jobs = []sourceagent.ControllerJob{}
|
|
}
|
|
writeJSON(w, 200, map[string]any{"jobs": jobs, "stats": s.SourceAgents.ControllerStats(ctx)})
|
|
}
|
|
func (s *Server) handleCreateControllerJob(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
var job sourceagent.ControllerJob
|
|
if err := decode(r, &job); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
job.Autonomous = false
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
out, err := s.SourceAgents.QueueControllerJob(ctx, job)
|
|
if err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if s.Broker != nil {
|
|
s.Broker.Publish(model.Activity{Type: "controller.job.queued", Source: "brain", Phase: "controller-manual", Message: "Manueller Docker-Controller-Job wurde eingeplant", Strength: .7, Metadata: map[string]any{"job_id": out.ID, "kind": out.Kind, "agent_id": out.AgentID, "dry_run": out.DryRun}})
|
|
}
|
|
writeJSON(w, http.StatusAccepted, out)
|
|
}
|
|
func (s *Server) handleCancelControllerJob(w http.ResponseWriter, r *http.Request) {
|
|
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
if err := s.SourceAgents.CancelControllerJob(ctx, r.PathValue("id")); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
func agentHasCapability(a sourceagent.Agent, capability string) bool {
|
|
for _, c := range a.Capabilities {
|
|
if c == capability {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func (s *Server) handleAgentControllerClaim(w http.ResponseWriter, r *http.Request) {
|
|
a, err := s.authenticateSourceAgent(r)
|
|
if err != nil {
|
|
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
|
|
return
|
|
}
|
|
if !agentHasCapability(a, sourceagent.CapabilityDockerController) {
|
|
writeJSON(w, 403, map[string]string{"error": "agent has not advertised docker_controller capability"})
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 15*time.Second)
|
|
defer cancel()
|
|
p, err := s.SourceAgents.ControllerPolicy(ctx)
|
|
if err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !p.Enabled {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
job, ok, err := s.SourceAgents.ClaimControllerJob(ctx, a.ID, p.MaxJobDuration, agentHasCapability(a, sourceagent.CapabilityDockerCompose))
|
|
if err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !ok {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
writeJSON(w, 200, sourceagent.ControllerClaim{SchemaVersion: sourceagent.SchemaVersion, Job: job, Policy: p})
|
|
}
|
|
func (s *Server) handleAgentControllerAuthorized(w http.ResponseWriter, r *http.Request) {
|
|
a, err := s.authenticateSourceAgent(r)
|
|
if err != nil {
|
|
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 5*time.Second)
|
|
defer cancel()
|
|
ok, reason, err := s.SourceAgents.ControllerJobAuthorized(ctx, a.ID, r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !ok {
|
|
writeJSON(w, 409, map[string]any{"authorized": false, "reason": reason})
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"authorized": true})
|
|
}
|
|
func (s *Server) handleAgentControllerResult(w http.ResponseWriter, r *http.Request) {
|
|
a, err := s.authenticateSourceAgent(r)
|
|
if err != nil {
|
|
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
|
|
return
|
|
}
|
|
var result sourceagent.ControllerJobResult
|
|
dec := json.NewDecoder(io.LimitReader(r.Body, 2<<20))
|
|
if err := dec.Decode(&result); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
result.JobID = strings.TrimSpace(r.PathValue("id"))
|
|
if result.AgentID != "" && result.AgentID != a.ID {
|
|
writeJSON(w, 403, map[string]string{"error": "agent_id mismatch"})
|
|
return
|
|
}
|
|
ctx, cancel := contextTimeout(r, 10*time.Second)
|
|
defer cancel()
|
|
job, _ := s.SourceAgents.ControllerJob(ctx, result.JobID)
|
|
if err := s.SourceAgents.CompleteControllerJob(ctx, a.ID, result); err != nil {
|
|
writeJSON(w, 409, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
nodeIDs := controllerJobNodeIDs(job)
|
|
if job.Kind == "evidence_http_probe" && result.Status == sourceagent.ControllerJobSucceeded && result.Error == "" && s.Graph != nil {
|
|
if external, ok := s.Graph.LookupExternal(strings.TrimSpace(asControllerString(job.Parameters["url"]))); ok {
|
|
if external.Metadata == nil {
|
|
external.Metadata = map[string]any{}
|
|
}
|
|
external.Metadata["controller_probe_verified_at"] = time.Now().UTC().Format(time.RFC3339Nano)
|
|
external.Metadata["controller_probe_agent_id"] = a.ID
|
|
external.Metadata["controller_probe_job_id"] = result.JobID
|
|
for _, key := range []string{"http_status", "body_sha256", "captured_bytes", "effective_url", "content_type", "download_bytes"} {
|
|
if value, exists := result.Result[key]; exists {
|
|
external.Metadata["controller_probe_"+key] = value
|
|
}
|
|
}
|
|
s.Graph.UpsertNode(external)
|
|
nodeIDs = append(nodeIDs, external.ID)
|
|
}
|
|
}
|
|
if s.Broker != nil {
|
|
s.Broker.Publish(model.Activity{Type: "controller.job.completed", Source: "agent", Phase: "controller", NodeIDs: nodeIDs, Message: "Docker-Controller-Job wurde abgeschlossen", Strength: .76, Metadata: map[string]any{"job_id": result.JobID, "agent_id": a.ID, "kind": job.Kind, "status": result.Status, "duration_ms": result.DurationMS, "error": result.Error, "result": result.Result}})
|
|
}
|
|
writeJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
func asControllerString(v any) string {
|
|
return strings.TrimSpace(strings.TrimSpace(toControllerString(v)))
|
|
}
|
|
func toControllerString(v any) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
func controllerJobNodeIDs(job sourceagent.ControllerJob) []string {
|
|
var out []string
|
|
switch raw := job.Parameters["source_node_ids"].(type) {
|
|
case []string:
|
|
out = append(out, raw...)
|
|
case []any:
|
|
for _, v := range raw {
|
|
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
|
|
out = append(out, strings.TrimSpace(s))
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|