mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:11:31 +00:00
995b72df05
Replace static env-var-based issuer wiring with GUI-driven dynamic configuration stored encrypted in PostgreSQL. Operators can now configure, test, enable/disable, and manage issuers from the dashboard without restarting the server. Key changes: - AES-256-GCM encryption for sensitive issuer config at rest (PBKDF2 key derivation with 100k iterations) - Dynamic IssuerRegistry with sync.RWMutex replacing static map - Connector factory pattern (issuerfactory.NewFromConfig) replacing 140 lines of static wiring in main.go - Migration 000009: encrypted_config, last_tested_at, test_status, source columns on issuers table - Env var seeding on first boot with ON CONFLICT DO NOTHING - Registry Rebuild() for atomic map swap after CRUD operations - Issuer type validation against domain constants on Create - Audit trail for test connection results - Conditional seeding for step-ca/OpenSSL (only when env vars set) - GUI: source badge, connection test status on issuer detail page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
140 lines
3.8 KiB
Go
140 lines
3.8 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/issuerfactory"
|
|
"github.com/shankar0123/certctl/internal/crypto"
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// IssuerRegistry is a thread-safe registry of issuer connectors.
|
|
// It replaces the static map[string]IssuerConnector that was built at startup.
|
|
// Consumers call Get() to look up a connector by issuer ID.
|
|
type IssuerRegistry struct {
|
|
mu sync.RWMutex
|
|
issuers map[string]IssuerConnector
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewIssuerRegistry creates a new empty issuer registry.
|
|
func NewIssuerRegistry(logger *slog.Logger) *IssuerRegistry {
|
|
return &IssuerRegistry{
|
|
issuers: make(map[string]IssuerConnector),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Get returns the issuer connector for the given ID and whether it exists.
|
|
func (r *IssuerRegistry) Get(id string) (IssuerConnector, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
conn, ok := r.issuers[id]
|
|
return conn, ok
|
|
}
|
|
|
|
// Set adds or replaces an issuer connector in the registry.
|
|
func (r *IssuerRegistry) Set(id string, conn IssuerConnector) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.issuers[id] = conn
|
|
}
|
|
|
|
// Remove removes an issuer connector from the registry.
|
|
func (r *IssuerRegistry) Remove(id string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.issuers, id)
|
|
}
|
|
|
|
// List returns a copy of all registered issuers.
|
|
func (r *IssuerRegistry) List() map[string]IssuerConnector {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
result := make(map[string]IssuerConnector, len(r.issuers))
|
|
for k, v := range r.issuers {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Len returns the number of registered issuers.
|
|
func (r *IssuerRegistry) Len() int {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return len(r.issuers)
|
|
}
|
|
|
|
// Rebuild reconstructs the registry from a list of issuer configs.
|
|
// For each enabled issuer, it decrypts the config (if encryption key is set),
|
|
// instantiates a connector via the factory, wraps it in an adapter, and
|
|
// atomically swaps the entire map.
|
|
func (r *IssuerRegistry) Rebuild(configs []*domain.Issuer, encryptionKey []byte) error {
|
|
newIssuers := make(map[string]IssuerConnector)
|
|
var errors []string
|
|
|
|
for _, cfg := range configs {
|
|
if !cfg.Enabled {
|
|
r.logger.Debug("skipping disabled issuer", "id", cfg.ID, "type", cfg.Type)
|
|
continue
|
|
}
|
|
|
|
// Determine the config JSON to use for connector instantiation.
|
|
// Prefer encrypted_config (decrypted) if available; fall back to config.
|
|
var configJSON json.RawMessage
|
|
if len(cfg.EncryptedConfig) > 0 {
|
|
decrypted, err := crypto.DecryptIfKeySet(cfg.EncryptedConfig, encryptionKey)
|
|
if err != nil {
|
|
errors = append(errors, fmt.Sprintf("issuer %s: decrypt failed: %v", cfg.ID, err))
|
|
continue
|
|
}
|
|
configJSON = json.RawMessage(decrypted)
|
|
} else if len(cfg.Config) > 0 {
|
|
configJSON = cfg.Config
|
|
} else {
|
|
configJSON = json.RawMessage("{}")
|
|
}
|
|
|
|
connector, err := issuerfactory.NewFromConfig(string(cfg.Type), configJSON, r.logger)
|
|
if err != nil {
|
|
errors = append(errors, fmt.Sprintf("issuer %s: factory error: %v", cfg.ID, err))
|
|
continue
|
|
}
|
|
|
|
newIssuers[cfg.ID] = NewIssuerConnectorAdapter(connector)
|
|
r.logger.Info("issuer loaded into registry", "id", cfg.ID, "type", cfg.Type)
|
|
}
|
|
|
|
// Atomic swap
|
|
r.mu.Lock()
|
|
old := r.issuers
|
|
r.issuers = newIssuers
|
|
r.mu.Unlock()
|
|
|
|
// Log changes
|
|
for id := range newIssuers {
|
|
if _, existed := old[id]; !existed {
|
|
r.logger.Info("issuer added to registry", "id", id)
|
|
}
|
|
}
|
|
for id := range old {
|
|
if _, exists := newIssuers[id]; !exists {
|
|
r.logger.Info("issuer removed from registry", "id", id)
|
|
}
|
|
}
|
|
|
|
r.logger.Info("issuer registry rebuilt", "loaded", len(newIssuers), "failed", len(errors))
|
|
|
|
if len(errors) > 0 {
|
|
for _, e := range errors {
|
|
r.logger.Warn("issuer load failure", "detail", e)
|
|
}
|
|
return fmt.Errorf("%d issuer(s) failed to load: %s", len(errors), errors[0])
|
|
}
|
|
|
|
return nil
|
|
}
|