mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Router & Middleware: - Add auth context middleware for user/token extraction - Add tenant middleware with authorization checking - Refactor middleware chain ordering for proper isolation - Add router helpers for common patterns Authentication & SSO: - Enhance auth with tenant-aware context - Update OIDC, SAML, and SSO handlers for multi-tenant - Add RBAC handler improvements - Add security enhancements New Test Coverage: - API foundation tests - Auth and authorization tests - Router state and general tests - SSO handler CRUD tests - WebSocket isolation tests - Resource handler tests
1001 lines
29 KiB
Go
1001 lines
29 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// ConfigProfileHandler handles configuration profile operations
|
|
type ConfigProfileHandler struct {
|
|
mtPersistence *config.MultiTenantPersistence
|
|
validator *models.ProfileValidator
|
|
mu sync.RWMutex
|
|
suggestionHandler *ProfileSuggestionHandler
|
|
}
|
|
|
|
// NewConfigProfileHandler creates a new handler
|
|
func NewConfigProfileHandler(mtp *config.MultiTenantPersistence) *ConfigProfileHandler {
|
|
return &ConfigProfileHandler{
|
|
mtPersistence: mtp,
|
|
validator: models.NewProfileValidator(),
|
|
}
|
|
}
|
|
|
|
// getPersistence resolves the persistence instance for the current tenant
|
|
func (h *ConfigProfileHandler) getPersistence(ctx context.Context) (*config.ConfigPersistence, error) {
|
|
orgID := GetOrgID(ctx)
|
|
return h.mtPersistence.GetPersistence(orgID)
|
|
}
|
|
|
|
// SetAIHandler sets the AI handler for profile suggestions
|
|
func (h *ConfigProfileHandler) SetAIHandler(aiHandler *AIHandler) {
|
|
// We pass nil for persistence here because the suggestion handler will need
|
|
// to use the context-aware persistence, which requires deeper refactoring of ProfileSuggestionHandler.
|
|
// For now, we'll let ProfileSuggestionHandler resolve persistence from AIHandler if possible,
|
|
// or we update ProfileSuggestionHandler to be multi-tenant aware as well.
|
|
// Actually, ProfileSuggestionHandler needs persistence. Let's look at that separately.
|
|
// For this step, we'll temporarilly break this or pass nil and fix it in the next step.
|
|
// A better approach: ProfileSuggestionHandler should take MultiTenantPersistence too.
|
|
// Let's assume we update ProfileSuggestionHandler next.
|
|
h.suggestionHandler = NewProfileSuggestionHandler(nil, aiHandler)
|
|
}
|
|
|
|
// ServeHTTP implements the http.Handler interface
|
|
func (h *ConfigProfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Simple routing
|
|
path := strings.TrimSuffix(r.URL.Path, "/")
|
|
|
|
if path == "" || path == "/" {
|
|
if r.Method == http.MethodGet {
|
|
h.ListProfiles(w, r)
|
|
return
|
|
} else if r.Method == http.MethodPost {
|
|
h.CreateProfile(w, r)
|
|
return
|
|
}
|
|
} else if path == "/assignments" {
|
|
if r.Method == http.MethodGet {
|
|
h.ListAssignments(w, r)
|
|
return
|
|
} else if r.Method == http.MethodPost {
|
|
h.AssignProfile(w, r)
|
|
return
|
|
}
|
|
} else if strings.HasPrefix(path, "/assignments/") {
|
|
if r.Method == http.MethodDelete {
|
|
agentID := strings.TrimPrefix(path, "/assignments/")
|
|
h.UnassignProfile(w, r, agentID)
|
|
return
|
|
}
|
|
} else if path == "/schema" {
|
|
// GET /schema - Return config key definitions
|
|
if r.Method == http.MethodGet {
|
|
h.GetConfigSchema(w, r)
|
|
return
|
|
}
|
|
} else if path == "/validate" {
|
|
// POST /validate - Validate a config without saving
|
|
if r.Method == http.MethodPost {
|
|
h.ValidateConfig(w, r)
|
|
return
|
|
}
|
|
} else if path == "/suggestions" {
|
|
// POST /suggestions - AI-assisted profile suggestion
|
|
if r.Method == http.MethodPost {
|
|
if h.suggestionHandler != nil {
|
|
h.suggestionHandler.HandleSuggestProfile(w, r)
|
|
} else {
|
|
http.Error(w, "Pulse Assistant service not configured", http.StatusServiceUnavailable)
|
|
}
|
|
return
|
|
}
|
|
} else if path == "/changelog" {
|
|
// GET /changelog - Return profile change history
|
|
if r.Method == http.MethodGet {
|
|
h.GetChangeLog(w, r)
|
|
return
|
|
}
|
|
} else if path == "/deployments" {
|
|
// GET /deployments - Return deployment status
|
|
// POST /deployments - Update deployment status from agent
|
|
if r.Method == http.MethodGet {
|
|
h.GetDeploymentStatus(w, r)
|
|
return
|
|
} else if r.Method == http.MethodPost {
|
|
h.UpdateDeploymentStatus(w, r)
|
|
return
|
|
}
|
|
} else if strings.HasSuffix(path, "/versions") {
|
|
// GET /{id}/versions - Get version history for a profile
|
|
id := strings.TrimSuffix(path, "/versions")
|
|
id = strings.TrimPrefix(id, "/")
|
|
if r.Method == http.MethodGet {
|
|
h.GetProfileVersions(w, r, id)
|
|
return
|
|
}
|
|
} else if strings.Contains(path, "/rollback/") {
|
|
// POST /{id}/rollback/{version} - Rollback to a specific version
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) >= 3 && r.Method == http.MethodPost {
|
|
// parts: ["", "id", "rollback", "version"]
|
|
id := parts[1]
|
|
version := parts[len(parts)-1]
|
|
h.RollbackProfile(w, r, id, version)
|
|
return
|
|
}
|
|
} else {
|
|
// ID parameters
|
|
// Expecting /{id}
|
|
id := strings.TrimPrefix(path, "/")
|
|
if r.Method == http.MethodGet {
|
|
h.GetProfile(w, r, id)
|
|
return
|
|
} else if r.Method == http.MethodPut {
|
|
h.UpdateProfile(w, r, id)
|
|
return
|
|
} else if r.Method == http.MethodDelete {
|
|
h.DeleteProfile(w, r, id)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
}
|
|
|
|
// ListProfiles returns all profiles
|
|
func (h *ConfigProfileHandler) ListProfiles(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load profiles")
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Return empty array instead of null
|
|
if profiles == nil {
|
|
profiles = []models.AgentProfile{}
|
|
}
|
|
json.NewEncoder(w).Encode(profiles)
|
|
}
|
|
|
|
// CreateProfile creates a new profile
|
|
func (h *ConfigProfileHandler) CreateProfile(w http.ResponseWriter, r *http.Request) {
|
|
var input struct {
|
|
models.AgentProfile
|
|
ChangeNote string `json:"change_note,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if input.Name == "" {
|
|
http.Error(w, "Name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate configuration
|
|
if input.Config != nil {
|
|
result := h.validator.Validate(input.Config)
|
|
if !result.Valid {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"error": "validation_failed",
|
|
"message": "Configuration validation failed",
|
|
"errors": result.Errors,
|
|
"warnings": result.Warnings,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Get username from context
|
|
username := getUsernameFromRequest(r)
|
|
|
|
input.ID = uuid.New().String()
|
|
input.Version = 1
|
|
input.CreatedAt = time.Now()
|
|
input.UpdatedAt = time.Now()
|
|
input.CreatedBy = username
|
|
input.UpdatedBy = username
|
|
|
|
profiles = append(profiles, input.AgentProfile)
|
|
|
|
if err := persistence.SaveAgentProfiles(profiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles")
|
|
http.Error(w, "Failed to save profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Save initial version to history
|
|
version := models.AgentProfileVersion{
|
|
ProfileID: input.ID,
|
|
Version: 1,
|
|
Name: input.Name,
|
|
Description: input.Description,
|
|
Config: input.Config,
|
|
ParentID: input.ParentID,
|
|
CreatedAt: input.CreatedAt,
|
|
CreatedBy: username,
|
|
ChangeNote: input.ChangeNote,
|
|
}
|
|
h.saveVersionHistory(persistence, version)
|
|
|
|
// Log change
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: input.ID,
|
|
ProfileName: input.Name,
|
|
Action: "create",
|
|
NewVersion: 1,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
Details: input.ChangeNote,
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(input.AgentProfile)
|
|
}
|
|
|
|
// getUsernameFromRequest extracts the username from the request context
|
|
func getUsernameFromRequest(r *http.Request) string {
|
|
if username, ok := r.Context().Value("username").(string); ok {
|
|
return username
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// UpdateProfile updates an existing profile
|
|
func (h *ConfigProfileHandler) UpdateProfile(w http.ResponseWriter, r *http.Request, id string) {
|
|
if id == "" {
|
|
http.Error(w, "ID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var input struct {
|
|
models.AgentProfile
|
|
ChangeNote string `json:"change_note,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate configuration
|
|
if input.Config != nil {
|
|
result := h.validator.Validate(input.Config)
|
|
if !result.Valid {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"error": "validation_failed",
|
|
"message": "Configuration validation failed",
|
|
"errors": result.Errors,
|
|
"warnings": result.Warnings,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
username := getUsernameFromRequest(r)
|
|
found := false
|
|
var oldVersion int
|
|
var updatedProfile models.AgentProfile
|
|
|
|
for i, p := range profiles {
|
|
if p.ID == id {
|
|
oldVersion = p.Version
|
|
profiles[i].Name = input.Name
|
|
profiles[i].Description = input.Description
|
|
profiles[i].Config = input.Config
|
|
profiles[i].ParentID = input.ParentID
|
|
profiles[i].Version = p.Version + 1
|
|
profiles[i].UpdatedAt = time.Now()
|
|
profiles[i].UpdatedBy = username
|
|
updatedProfile = profiles[i]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := persistence.SaveAgentProfiles(profiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles")
|
|
http.Error(w, "Failed to save profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Save new version to history
|
|
version := models.AgentProfileVersion{
|
|
ProfileID: id,
|
|
Version: updatedProfile.Version,
|
|
Name: updatedProfile.Name,
|
|
Description: updatedProfile.Description,
|
|
Config: updatedProfile.Config,
|
|
ParentID: updatedProfile.ParentID,
|
|
CreatedAt: updatedProfile.UpdatedAt,
|
|
CreatedBy: username,
|
|
ChangeNote: input.ChangeNote,
|
|
}
|
|
h.saveVersionHistory(persistence, version)
|
|
|
|
// Log change
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: id,
|
|
ProfileName: updatedProfile.Name,
|
|
Action: "update",
|
|
OldVersion: oldVersion,
|
|
NewVersion: updatedProfile.Version,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
Details: input.ChangeNote,
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(updatedProfile)
|
|
}
|
|
|
|
// DeleteProfile deletes a profile
|
|
func (h *ConfigProfileHandler) DeleteProfile(w http.ResponseWriter, r *http.Request, id string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var deletedProfile *models.AgentProfile
|
|
newProfiles := []models.AgentProfile{}
|
|
for _, p := range profiles {
|
|
if p.ID != id {
|
|
newProfiles = append(newProfiles, p)
|
|
} else {
|
|
deletedProfile = &p
|
|
}
|
|
}
|
|
|
|
if len(newProfiles) == len(profiles) {
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := persistence.SaveAgentProfiles(newProfiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles")
|
|
http.Error(w, "Failed to delete profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load assignments for profile cleanup")
|
|
http.Error(w, "Failed to delete profile assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
cleaned := assignments[:0]
|
|
for _, a := range assignments {
|
|
if a.ProfileID != id {
|
|
cleaned = append(cleaned, a)
|
|
}
|
|
}
|
|
|
|
if len(cleaned) != len(assignments) {
|
|
if err := persistence.SaveAgentProfileAssignments(cleaned); err != nil {
|
|
log.Error().Err(err).Msg("Failed to clean up assignments for deleted profile")
|
|
http.Error(w, "Failed to delete profile assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Log deletion
|
|
username := getUsernameFromRequest(r)
|
|
if deletedProfile != nil {
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: id,
|
|
ProfileName: deletedProfile.Name,
|
|
Action: "delete",
|
|
OldVersion: deletedProfile.Version,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// ListAssignments returns all assignments
|
|
func (h *ConfigProfileHandler) ListAssignments(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load assignments")
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Return empty array instead of null
|
|
if assignments == nil {
|
|
assignments = []models.AgentProfileAssignment{}
|
|
}
|
|
json.NewEncoder(w).Encode(assignments)
|
|
}
|
|
|
|
// AssignProfile assigns a profile to an agent
|
|
func (h *ConfigProfileHandler) AssignProfile(w http.ResponseWriter, r *http.Request) {
|
|
var input models.AgentProfileAssignment
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if input.AgentID == "" || input.ProfileID == "" {
|
|
http.Error(w, "AgentID and ProfileID are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Remove existing assignment for this agent if exists
|
|
newAssignments := []models.AgentProfileAssignment{}
|
|
for _, a := range assignments {
|
|
if a.AgentID != input.AgentID {
|
|
newAssignments = append(newAssignments, a)
|
|
}
|
|
}
|
|
|
|
username := getUsernameFromRequest(r)
|
|
input.UpdatedAt = time.Now()
|
|
input.AssignedBy = username
|
|
newAssignments = append(newAssignments, input)
|
|
|
|
if err := persistence.SaveAgentProfileAssignments(newAssignments); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save assignments")
|
|
http.Error(w, "Failed to save assignment", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Get profile name for logging
|
|
profiles, _ := persistence.LoadAgentProfiles()
|
|
var profileName string
|
|
for _, p := range profiles {
|
|
if p.ID == input.ProfileID {
|
|
profileName = p.Name
|
|
break
|
|
}
|
|
}
|
|
|
|
// Log assignment
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: input.ProfileID,
|
|
ProfileName: profileName,
|
|
Action: "assign",
|
|
AgentID: input.AgentID,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
tokenID := ""
|
|
if record := getAPITokenRecordFromRequest(r); record != nil {
|
|
tokenID = record.ID
|
|
}
|
|
LogAuditEventForTenant(GetOrgID(r.Context()), "agent_profile_assigned", username, GetClientIP(r), r.URL.Path, true,
|
|
fmt.Sprintf("agent_id=%s profile_id=%s token_id=%s", input.AgentID, input.ProfileID, tokenID))
|
|
|
|
json.NewEncoder(w).Encode(input)
|
|
}
|
|
|
|
// UnassignProfile removes a profile assignment for an agent.
|
|
func (h *ConfigProfileHandler) UnassignProfile(w http.ResponseWriter, r *http.Request, agentID string) {
|
|
agentID = strings.TrimSpace(agentID)
|
|
if agentID == "" {
|
|
http.Error(w, "AgentID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var removedAssignment *models.AgentProfileAssignment
|
|
newAssignments := []models.AgentProfileAssignment{}
|
|
for _, a := range assignments {
|
|
if a.AgentID != agentID {
|
|
newAssignments = append(newAssignments, a)
|
|
} else {
|
|
removedAssignment = &a
|
|
}
|
|
}
|
|
|
|
if len(newAssignments) != len(assignments) {
|
|
if err := persistence.SaveAgentProfileAssignments(newAssignments); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save assignments")
|
|
http.Error(w, "Failed to save assignment", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Log unassignment
|
|
if removedAssignment != nil {
|
|
username := getUsernameFromRequest(r)
|
|
|
|
// Get profile name for logging
|
|
profiles, _ := persistence.LoadAgentProfiles()
|
|
var profileName string
|
|
for _, p := range profiles {
|
|
if p.ID == removedAssignment.ProfileID {
|
|
profileName = p.Name
|
|
break
|
|
}
|
|
}
|
|
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: removedAssignment.ProfileID,
|
|
ProfileName: profileName,
|
|
Action: "unassign",
|
|
AgentID: agentID,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
tokenID := ""
|
|
if record := getAPITokenRecordFromRequest(r); record != nil {
|
|
tokenID = record.ID
|
|
}
|
|
LogAuditEventForTenant(GetOrgID(r.Context()), "agent_profile_unassigned", username, GetClientIP(r), r.URL.Path, true,
|
|
fmt.Sprintf("agent_id=%s profile_id=%s token_id=%s", agentID, removedAssignment.ProfileID, tokenID))
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// GetProfile returns a single profile by ID
|
|
func (h *ConfigProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request, id string) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load profiles")
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
for _, p := range profiles {
|
|
if p.ID == id {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(p)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
}
|
|
|
|
// GetConfigSchema returns the configuration key definitions
|
|
func (h *ConfigProfileHandler) GetConfigSchema(w http.ResponseWriter, r *http.Request) {
|
|
definitions := models.GetConfigKeyDefinitions()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(definitions)
|
|
}
|
|
|
|
// ValidateConfig validates a configuration without saving
|
|
func (h *ConfigProfileHandler) ValidateConfig(w http.ResponseWriter, r *http.Request) {
|
|
var config models.AgentConfigMap
|
|
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result := h.validator.Validate(config)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// GetChangeLog returns profile change history
|
|
func (h *ConfigProfileHandler) GetChangeLog(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
logs, err := persistence.LoadProfileChangeLogs()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load change logs")
|
|
http.Error(w, "Failed to load change logs", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Filter by profile_id if specified
|
|
profileID := r.URL.Query().Get("profile_id")
|
|
if profileID != "" {
|
|
filtered := []models.ProfileChangeLog{}
|
|
for _, entry := range logs {
|
|
if entry.ProfileID == profileID {
|
|
filtered = append(filtered, entry)
|
|
}
|
|
}
|
|
logs = filtered
|
|
}
|
|
|
|
// Return empty array instead of null
|
|
if logs == nil {
|
|
logs = []models.ProfileChangeLog{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(logs)
|
|
}
|
|
|
|
// GetDeploymentStatus returns deployment status for all agents
|
|
func (h *ConfigProfileHandler) GetDeploymentStatus(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
status, err := persistence.LoadProfileDeploymentStatus()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load deployment status")
|
|
http.Error(w, "Failed to load deployment status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Filter by agent_id if specified
|
|
agentID := r.URL.Query().Get("agent_id")
|
|
if agentID != "" {
|
|
filtered := []models.ProfileDeploymentStatus{}
|
|
for _, s := range status {
|
|
if s.AgentID == agentID {
|
|
filtered = append(filtered, s)
|
|
}
|
|
}
|
|
status = filtered
|
|
}
|
|
|
|
// Return empty array instead of null
|
|
if status == nil {
|
|
status = []models.ProfileDeploymentStatus{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(status)
|
|
}
|
|
|
|
// UpdateDeploymentStatus updates deployment status from an agent
|
|
func (h *ConfigProfileHandler) UpdateDeploymentStatus(w http.ResponseWriter, r *http.Request) {
|
|
var input models.ProfileDeploymentStatus
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if input.AgentID == "" || input.ProfileID == "" {
|
|
http.Error(w, "AgentID and ProfileID are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate deployment status
|
|
validStatuses := []string{"pending", "deployed", "failed"}
|
|
validStatus := false
|
|
for _, s := range validStatuses {
|
|
if input.DeploymentStatus == s {
|
|
validStatus = true
|
|
break
|
|
}
|
|
}
|
|
if !validStatus {
|
|
http.Error(w, "Invalid deployment status. Must be 'pending', 'deployed', or 'failed'", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
statuses, err := persistence.LoadProfileDeploymentStatus()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load deployment status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update or add the status
|
|
found := false
|
|
for i, s := range statuses {
|
|
if s.AgentID == input.AgentID && s.ProfileID == input.ProfileID {
|
|
statuses[i].DeployedVersion = input.DeployedVersion
|
|
statuses[i].DeploymentStatus = input.DeploymentStatus
|
|
statuses[i].ErrorMessage = input.ErrorMessage
|
|
statuses[i].LastDeployedAt = time.Now()
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
input.LastDeployedAt = time.Now()
|
|
statuses = append(statuses, input)
|
|
}
|
|
|
|
if err := persistence.SaveProfileDeploymentStatus(statuses); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save deployment status")
|
|
http.Error(w, "Failed to save deployment status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(input)
|
|
}
|
|
|
|
// GetProfileVersions returns version history for a profile
|
|
func (h *ConfigProfileHandler) GetProfileVersions(w http.ResponseWriter, r *http.Request, profileID string) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
versions, err := persistence.LoadAgentProfileVersions()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load profile versions")
|
|
http.Error(w, "Failed to load profile versions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Filter by profile ID
|
|
filtered := []models.AgentProfileVersion{}
|
|
for _, v := range versions {
|
|
if v.ProfileID == profileID {
|
|
filtered = append(filtered, v)
|
|
}
|
|
}
|
|
|
|
// Return empty array instead of null
|
|
if filtered == nil {
|
|
filtered = []models.AgentProfileVersion{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(filtered)
|
|
}
|
|
|
|
// RollbackProfile rolls back a profile to a specific version
|
|
func (h *ConfigProfileHandler) RollbackProfile(w http.ResponseWriter, r *http.Request, profileID string, versionStr string) {
|
|
var targetVersion int
|
|
if _, err := fmt.Sscanf(versionStr, "%d", &targetVersion); err != nil {
|
|
http.Error(w, "Invalid version number", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Load version history to find the target version
|
|
versions, err := persistence.LoadAgentProfileVersions()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profile versions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var targetVersionData *models.AgentProfileVersion
|
|
for i, v := range versions {
|
|
if v.ProfileID == profileID && v.Version == targetVersion {
|
|
targetVersionData = &versions[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if targetVersionData == nil {
|
|
http.Error(w, "Version not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Load current profiles
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
username := getUsernameFromRequest(r)
|
|
found := false
|
|
var oldVersion int
|
|
var updatedProfile models.AgentProfile
|
|
|
|
for i, p := range profiles {
|
|
if p.ID == profileID {
|
|
oldVersion = p.Version
|
|
profiles[i].Name = targetVersionData.Name
|
|
profiles[i].Description = targetVersionData.Description
|
|
profiles[i].Config = targetVersionData.Config
|
|
profiles[i].ParentID = targetVersionData.ParentID
|
|
profiles[i].Version = p.Version + 1
|
|
profiles[i].UpdatedAt = time.Now()
|
|
profiles[i].UpdatedBy = username
|
|
updatedProfile = profiles[i]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := persistence.SaveAgentProfiles(profiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles after rollback")
|
|
http.Error(w, "Failed to rollback profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Save new version to history
|
|
version := models.AgentProfileVersion{
|
|
ProfileID: profileID,
|
|
Version: updatedProfile.Version,
|
|
Name: updatedProfile.Name,
|
|
Description: updatedProfile.Description,
|
|
Config: updatedProfile.Config,
|
|
ParentID: updatedProfile.ParentID,
|
|
CreatedAt: updatedProfile.UpdatedAt,
|
|
CreatedBy: username,
|
|
ChangeNote: fmt.Sprintf("Rolled back to version %d", targetVersion),
|
|
}
|
|
h.saveVersionHistory(persistence, version)
|
|
|
|
// Log rollback
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: profileID,
|
|
ProfileName: updatedProfile.Name,
|
|
Action: "rollback",
|
|
OldVersion: oldVersion,
|
|
NewVersion: updatedProfile.Version,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
Details: fmt.Sprintf("Rolled back from version %d to version %d", oldVersion, targetVersion),
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(updatedProfile)
|
|
}
|
|
|
|
// saveVersionHistory saves a version to the history
|
|
func (h *ConfigProfileHandler) saveVersionHistory(persistence *config.ConfigPersistence, version models.AgentProfileVersion) {
|
|
versions, err := persistence.LoadAgentProfileVersions()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load version history")
|
|
return
|
|
}
|
|
|
|
versions = append(versions, version)
|
|
|
|
if err := persistence.SaveAgentProfileVersions(versions); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save version history")
|
|
}
|
|
}
|
|
|
|
// logChange logs a profile change to the change log
|
|
func (h *ConfigProfileHandler) logChange(persistence *config.ConfigPersistence, entry models.ProfileChangeLog) {
|
|
if err := persistence.AppendProfileChangeLog(entry); err != nil {
|
|
log.Error().Err(err).Msg("Failed to log profile change")
|
|
}
|
|
}
|