Files
Pulse/internal/api/config_profiles.go
rcourtman 9072b8eaa8 feat: enhance API router with multi-tenant authorization
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
2026-01-24 22:42:23 +00:00

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")
}
}