// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package peers serves the trusted-services surface: pending first-contact // authorizations and the approve/revoke actions for the instance owner // (security theme M2 §3). Maestro is the UI; the auth service owns the // enforcement. package peers import ( "encoding/json" "net/http" "sync" "git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt" ) // Handler wraps a lazily-initialized auth management client so identity // issuance happens once per process, not per request. type Handler struct { mu sync.Mutex client *authmgmt.Client } // NewHandler creates the peers handler. The underlying mTLS client is // constructed on first use. func NewHandler() *Handler { return &Handler{} } // List serves GET /api/peers — the combined pending + approved trust state. func (h *Handler) List(w http.ResponseWriter, r *http.Request) { client, err := h.ensureClient() if err != nil { http.Error(w, "auth management client unavailable: "+err.Error(), http.StatusInternalServerError) return } state, err := client.TrustState(r.Context()) if err != nil { http.Error(w, "failed to read trust state: "+err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, state) } // Attestation serves GET /api/peers/attestation — the auth service's public // signing key and its pinnable fingerprint. func (h *Handler) Attestation(w http.ResponseWriter, r *http.Request) { client, err := h.ensureClient() if err != nil { http.Error(w, "auth management client unavailable: "+err.Error(), http.StatusInternalServerError) return } att, err := client.Attestation(r.Context()) if err != nil { http.Error(w, "failed to read attestation: "+err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, att) } type authzAction struct { Service string `json:"service"` Fingerprint string `json:"fingerprint"` } // Approve serves POST /api/peers/approve — forwards the owner's approval to // the auth service's trust store. func (h *Handler) Approve(w http.ResponseWriter, r *http.Request) { var action authzAction if !decodeAction(r, w, &action) { return } client, err := h.ensureClient() if err != nil { http.Error(w, "auth management client unavailable: "+err.Error(), http.StatusInternalServerError) return } if err := client.Approve(r.Context(), action.Service, action.Fingerprint); err != nil { http.Error(w, "failed to approve peer: "+err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, map[string]string{"status": "approved"}) } // Revoke serves POST /api/peers/revoke — forwards the owner's revocation // (returns the peer to the no-op path immediately). func (h *Handler) Revoke(w http.ResponseWriter, r *http.Request) { var action authzAction if !decodeAction(r, w, &action) { return } client, err := h.ensureClient() if err != nil { http.Error(w, "auth management client unavailable: "+err.Error(), http.StatusInternalServerError) return } if err := client.Revoke(r.Context(), action.Service, action.Fingerprint); err != nil { http.Error(w, "failed to revoke peer: "+err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"}) } // Reject serves POST /api/peers/reject — denies a pending first-contact // request without authorizing it (the auth service drops the request and // audits the decision). func (h *Handler) Reject(w http.ResponseWriter, r *http.Request) { var action authzAction if !decodeAction(r, w, &action) { return } client, err := h.ensureClient() if err != nil { http.Error(w, "auth management client unavailable: "+err.Error(), http.StatusInternalServerError) return } if err := client.Reject(r.Context(), action.Service, action.Fingerprint); err != nil { http.Error(w, "failed to reject peer: "+err.Error(), http.StatusBadGateway) return } writeJSON(w, http.StatusOK, map[string]string{"status": "rejected"}) } func (h *Handler) ensureClient() (*authmgmt.Client, error) { h.mu.Lock() defer h.mu.Unlock() if h.client == nil { client, err := authmgmt.New() if err != nil { return nil, err } h.client = client } return h.client, nil } func decodeAction(r *http.Request, w http.ResponseWriter, out *authzAction) bool { if err := json.NewDecoder(r.Body).Decode(out); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return false } if out.Service == "" || out.Fingerprint == "" { http.Error(w, "service and fingerprint are required", http.StatusBadRequest) return false } return true } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }