Files
OrchestrAD/backend/internal/api/credentials_handlers.go
GraceSolutions 014003775b feat(lifecycle): add enable/disable endpoints for credentials, connections, and schedules
Add UpdateEnabled methods on CredentialRepository, ConnectionRepository, and ScheduleRepository that toggle the is_enabled flag while preserving updated_utc. Expose corresponding Enable/Disable handlers and wire POST /{id}/enable and POST /{id}/disable routes under /api/v1/credentials, /api/v1/ad-connections, and /api/v1/schedules. Each toggle emits an audit event. Also finalizes CredentialService.Test and TestCredentialInput so the existing /credentials/{id}/test handler compiles and runs against an LDAP host using the stored username and decrypted secret.
2026-04-23 13:35:08 -04:00

260 lines
9.4 KiB
Go

// Package api - Credentials handlers
package api
import (
"net/http"
"time"
"github.com/Grace-Solutions/OrchestrAD/internal/audit"
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
"github.com/Grace-Solutions/OrchestrAD/internal/models"
"github.com/Grace-Solutions/OrchestrAD/internal/services"
"github.com/go-chi/chi/v5"
)
// CredentialsHandler handles credential API endpoints
type CredentialsHandler struct {
service *services.CredentialService
auditService *audit.Service
logger *logging.Logger
}
// NewCredentialsHandler creates a new CredentialsHandler
func NewCredentialsHandler(service *services.CredentialService, auditService *audit.Service, logger *logging.Logger) *CredentialsHandler {
return &CredentialsHandler{
service: service,
auditService: auditService,
logger: logger,
}
}
// CredentialRequest represents a credential create/update request
type CredentialRequest struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
CredentialType string `json:"credentialType"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
IsEnabled *bool `json:"isEnabled,omitempty"`
}
// CredentialResponse represents a credential in responses
type CredentialResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
CredentialType string `json:"credentialType"`
Username *string `json:"username,omitempty"`
IsEnabled bool `json:"isEnabled"`
LastTestedAt *string `json:"lastTestedAt,omitempty"`
LastTestResult *string `json:"lastTestResult,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// List handles GET /api/v1/credentials
func (h *CredentialsHandler) List(w http.ResponseWriter, r *http.Request) {
p := ParsePagination(r)
creds, total, err := h.service.List(p.Page, p.PageSize)
if err != nil {
h.logger.Error("CredentialsHandler", "Failed to list credentials: %v", err)
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to list credentials")
return
}
var response []CredentialResponse
for _, c := range creds {
response = append(response, credentialToResponse(&c))
}
WriteList(w, response, p.Page, p.PageSize, total)
}
// Get handles GET /api/v1/credentials/{id}
func (h *CredentialsHandler) Get(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
cred, err := h.service.GetByID(id)
if err != nil {
h.logger.Error("CredentialsHandler", "Failed to get credential: %v", err)
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to get credential")
return
}
if cred == nil {
WriteError(w, http.StatusNotFound, ErrCodeNotFound, "Credential not found")
return
}
WriteJSON(w, http.StatusOK, credentialToResponse(cred))
}
// Create handles POST /api/v1/credentials
func (h *CredentialsHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CredentialRequest
if err := DecodeJSON(r, &req); err != nil {
WriteError(w, http.StatusBadRequest, ErrCodeBadRequest, "Invalid request body")
return
}
if req.Name == "" {
WriteError(w, http.StatusBadRequest, ErrCodeValidation, "Name is required")
return
}
cred, err := h.service.Create(services.CreateCredentialInput{
Name: req.Name,
Description: req.Description,
CredentialType: req.CredentialType,
Username: req.Username,
Password: req.Password,
})
if err != nil {
h.logger.Error("CredentialsHandler", "Failed to create credential: %v", err)
emitAudit(h.auditService, r, audit.EventCreate, "Credential", "", "Create", false,
map[string]any{"name": req.Name}, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to create credential")
return
}
emitAudit(h.auditService, r, audit.EventCreate, "Credential", cred.ID, "Create", true,
map[string]any{"name": cred.Name, "type": cred.CredentialType}, "")
WriteJSON(w, http.StatusCreated, credentialToResponse(cred))
}
// Update handles PUT /api/v1/credentials/{id}
func (h *CredentialsHandler) Update(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var req CredentialRequest
if err := DecodeJSON(r, &req); err != nil {
WriteError(w, http.StatusBadRequest, ErrCodeBadRequest, "Invalid request body")
return
}
cred, err := h.service.Update(services.UpdateCredentialInput{
ID: id,
Name: &req.Name,
Description: req.Description,
CredentialType: &req.CredentialType,
Username: req.Username,
Password: req.Password,
IsEnabled: req.IsEnabled,
})
if err != nil {
h.logger.Error("CredentialsHandler", "Failed to update credential: %v", err)
emitAudit(h.auditService, r, audit.EventUpdate, "Credential", id, "Update", false, nil, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to update credential")
return
}
emitAudit(h.auditService, r, audit.EventUpdate, "Credential", cred.ID, "Update", true,
map[string]any{"name": cred.Name}, "")
WriteJSON(w, http.StatusOK, credentialToResponse(cred))
}
// CredentialTestRequest represents a credential test request body
type CredentialTestRequest struct {
Host string `json:"host"`
Port int `json:"port,omitempty"`
UseTLS bool `json:"useTLS,omitempty"`
UseStartTLS bool `json:"useStartTLS,omitempty"`
AllowInvalidCerts bool `json:"allowInvalidCerts,omitempty"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
}
// Test handles POST /api/v1/credentials/{id}/test
func (h *CredentialsHandler) Test(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var req CredentialTestRequest
if err := DecodeJSON(r, &req); err != nil {
WriteError(w, http.StatusBadRequest, ErrCodeBadRequest, "Invalid request body")
return
}
if req.Host == "" {
WriteError(w, http.StatusBadRequest, ErrCodeValidation, "host is required")
return
}
result, err := h.service.Test(id, services.TestCredentialInput{
Host: req.Host,
Port: req.Port,
UseTLS: req.UseTLS,
UseStartTLS: req.UseStartTLS,
AllowInvalidCerts: req.AllowInvalidCerts,
TimeoutSeconds: req.TimeoutSeconds,
})
if err != nil {
h.logger.Error("CredentialsHandler", "Test failed: %v", err)
emitAudit(h.auditService, r, audit.EventTest, "Credential", id, "Test", false,
map[string]any{"host": req.Host}, err.Error())
status := http.StatusInternalServerError
code := ErrCodeInternalError
if err.Error() == "credential not found" {
status = http.StatusNotFound
code = ErrCodeNotFound
}
WriteError(w, status, code, err.Error())
return
}
emitAudit(h.auditService, r, audit.EventTest, "Credential", id, "Test", result.Success,
map[string]any{"host": req.Host}, "")
WriteJSON(w, http.StatusOK, result)
}
// Delete handles DELETE /api/v1/credentials/{id}
func (h *CredentialsHandler) Delete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.service.Delete(id); err != nil {
h.logger.Error("CredentialsHandler", "Failed to delete credential: %v", err)
emitAudit(h.auditService, r, audit.EventDelete, "Credential", id, "Delete", false, nil, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, err.Error())
return
}
emitAudit(h.auditService, r, audit.EventDelete, "Credential", id, "Delete", true, nil, "")
WriteJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
func credentialToResponse(c *models.Credential) CredentialResponse {
resp := CredentialResponse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
CredentialType: c.CredentialType,
Username: c.Username,
IsEnabled: c.IsEnabled,
LastTestResult: c.LastTestResult,
CreatedAt: c.CreatedUTC.Format(time.RFC3339),
UpdatedAt: c.UpdatedUTC.Format(time.RFC3339),
}
if c.LastTestedUTC != nil {
s := c.LastTestedUTC.Format(time.RFC3339)
resp.LastTestedAt = &s
}
return resp
}
// Enable handles POST /api/v1/credentials/{id}/enable
func (h *CredentialsHandler) Enable(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.service.SetEnabled(id, true); err != nil {
h.logger.Error("CredentialsHandler", "Enable failed: %v", err)
emitAudit(h.auditService, r, audit.EventUpdate, "Credential", id, "Enable", false, nil, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to enable credential")
return
}
emitAudit(h.auditService, r, audit.EventUpdate, "Credential", id, "Enable", true, map[string]any{"isEnabled": true}, "")
WriteJSON(w, http.StatusOK, map[string]bool{"enabled": true})
}
// Disable handles POST /api/v1/credentials/{id}/disable
func (h *CredentialsHandler) Disable(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.service.SetEnabled(id, false); err != nil {
h.logger.Error("CredentialsHandler", "Disable failed: %v", err)
emitAudit(h.auditService, r, audit.EventUpdate, "Credential", id, "Disable", false, nil, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to disable credential")
return
}
emitAudit(h.auditService, r, audit.EventUpdate, "Credential", id, "Disable", true, map[string]any{"isEnabled": false}, "")
WriteJSON(w, http.StatusOK, map[string]bool{"enabled": false})
}