Files
AetherForge/server/internal/api/spread_cred.go

199 lines
6.0 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strings"
dbpkg "crypto-miner-server/internal/db"
)
// DeploymentCredProfile is the API-facing deployment credential profile (no vault secrets).
type DeploymentCredProfile struct {
ID string `json:"id"`
Label string `json:"label"`
Username string `json:"username"`
VaultRef string `json:"vault_ref,omitempty"`
}
// SpreadCredProvider supplies authorized deployment credential profiles and vault secrets.
type SpreadCredProvider interface {
DeploymentProfiles() []DeploymentCredProfile
OrderProfilesForSubnet(subnet string, affinity []dbpkg.CredProfileAffinity) []DeploymentCredProfile
LoadProfileSecret(profileID string) (username, password string, err error)
}
type spreadCredIssueRequest struct {
AgentID string `json:"agent_id"`
Host string `json:"host"`
Subnet string `json:"subnet"`
Method string `json:"method"`
}
type spreadCredRedeemRequest struct {
Token string `json:"token"`
}
type spreadCredReportRequest struct {
AgentID string `json:"agent_id"`
Host string `json:"host"`
Subnet string `json:"subnet"`
CredentialProfileID string `json:"credential_profile_id"`
Method string `json:"method"`
Success bool `json:"success"`
}
// SpreadCredHandler issues short-lived bootstrap tokens and records cred graph edges.
type SpreadCredHandler struct {
db *dbpkg.Database
provider SpreadCredProvider
}
func NewSpreadCredHandler(database *dbpkg.Database, provider SpreadCredProvider) *SpreadCredHandler {
return &SpreadCredHandler{db: database, provider: provider}
}
// GET /api/v1/spread/credential-graph (alias: /api/v1/emberwake/cred-graph)
func (h *SpreadHandler) GetCredGraph(w http.ResponseWriter, r *http.Request) {
rows, err := h.db.ListCredGraphBySubnet()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if rows == nil {
rows = []dbpkg.CredGraphSubnetRow{}
}
writeJSON(w, map[string]interface{}{"subnets": rows})
}
// GET /api/v1/spread/service-graph?agent_id=&subnet=
func (h *SpreadHandler) GetServiceGraph(w http.ResponseWriter, r *http.Request) {
agentID := strings.TrimSpace(r.URL.Query().Get("agent_id"))
subnet := normalizeSubnetLabel(strings.TrimSpace(r.URL.Query().Get("subnet")))
services := []ServiceGraphEntry{}
if h.wsHub != nil {
services = h.wsHub.QueryServiceGraph(agentID, subnet)
}
resp := map[string]interface{}{"services": services}
if agentID != "" {
resp["agent_id"] = agentID
}
if subnet != "" {
resp["subnet"] = subnet
}
writeJSON(w, resp)
}
func normalizeSubnetLabel(subnet string) string {
subnet = strings.TrimSpace(subnet)
if strings.HasSuffix(subnet, ".x") {
return strings.TrimSuffix(subnet, ".x")
}
return subnet
}
// POST /api/v1/agent/spread-cred/issue
func (h *SpreadCredHandler) IssueToken(w http.ResponseWriter, r *http.Request) {
if h.provider == nil {
http.Error(w, "deployment credentials not configured", http.StatusNotFound)
return
}
var req spreadCredIssueRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
req.Host = strings.TrimSpace(req.Host)
req.Subnet = strings.TrimSpace(req.Subnet)
req.Method = strings.TrimSpace(req.Method)
req.AgentID = strings.TrimSpace(req.AgentID)
if req.Host == "" || req.Subnet == "" || req.Method == "" {
http.Error(w, "host, subnet, and method required", http.StatusBadRequest)
return
}
affinity, err := h.db.ListCredProfileAffinity(req.Subnet)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ordered := h.provider.OrderProfilesForSubnet(req.Subnet, affinity)
if len(ordered) == 0 {
http.Error(w, "no deployment credential profiles configured", http.StatusNotFound)
return
}
profile := ordered[0]
username, password, err := h.provider.LoadProfileSecret(profile.ID)
if err != nil {
http.Error(w, "credential vault unavailable", http.StatusServiceUnavailable)
return
}
token := issueSpreadCredToken(spreadCredTokenEntry{
AgentID: req.AgentID,
ProfileID: profile.ID,
Username: username,
Password: password,
Host: req.Host,
Subnet: req.Subnet,
Method: req.Method,
})
writeJSON(w, map[string]interface{}{
"token": token,
"profile_id": profile.ID,
"expires_in": int(spreadCredTokenTTL.Seconds()),
})
}
// POST /api/v1/agent/spread-cred/redeem
func (h *SpreadCredHandler) RedeemToken(w http.ResponseWriter, r *http.Request) {
var req spreadCredRedeemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
req.Token = strings.TrimSpace(req.Token)
if req.Token == "" {
http.Error(w, "token required", http.StatusBadRequest)
return
}
entry, ok := consumeSpreadCredToken(req.Token)
if !ok {
http.Error(w, "invalid or expired token", http.StatusUnauthorized)
return
}
writeJSON(w, map[string]interface{}{
"profile_id": entry.ProfileID,
"username": entry.Username,
"password": entry.Password,
"host": entry.Host,
"subnet": entry.Subnet,
"method": entry.Method,
})
}
// POST /api/v1/agent/spread-cred/report
func (h *SpreadCredHandler) ReportEdge(w http.ResponseWriter, r *http.Request) {
var req spreadCredReportRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
req.Host = strings.TrimSpace(req.Host)
req.Subnet = strings.TrimSpace(req.Subnet)
req.CredentialProfileID = strings.TrimSpace(req.CredentialProfileID)
req.Method = strings.TrimSpace(req.Method)
req.AgentID = strings.TrimSpace(req.AgentID)
if req.Host == "" || req.Subnet == "" || req.CredentialProfileID == "" {
http.Error(w, "host, subnet, and credential_profile_id required", http.StatusBadRequest)
return
}
if err := h.db.InsertCredEdge(req.Host, req.Subnet, req.CredentialProfileID, req.Method, req.AgentID, req.Success); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true})
}