[ci skip] more codex 5.3 improvements
This commit is contained in:
19
README.md
19
README.md
@@ -58,10 +58,12 @@ vctp -settings /path/to/vctp.yml -db-cleanup
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Database Configuration
|
## Database Configuration
|
||||||
By default the app uses SQLite and creates/opens `db.sqlite3`. You can opt into PostgreSQL
|
By default the app uses SQLite and creates/opens `db.sqlite3`.
|
||||||
by updating the settings file:
|
|
||||||
|
|
||||||
- `settings.database_driver`: `sqlite` (default) or `postgres`
|
PostgreSQL support is currently **experimental** and not a production target. To enable it,
|
||||||
|
set `VCTP_ENABLE_EXPERIMENTAL_POSTGRES=1` and update the settings file:
|
||||||
|
|
||||||
|
- `settings.database_driver`: `sqlite` (default) or `postgres` (experimental)
|
||||||
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
|
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -104,14 +106,19 @@ HTTP/TLS:
|
|||||||
|
|
||||||
vCenter:
|
vCenter:
|
||||||
- `settings.vcenter_username`: vCenter username
|
- `settings.vcenter_username`: vCenter username
|
||||||
- `settings.vcenter_password`: vCenter password (encrypted at startup)
|
- `settings.vcenter_password`: vCenter password (auto-encrypted on startup if plaintext length > 2)
|
||||||
- `settings.vcenter_insecure`: `true` to skip TLS verification
|
- `settings.vcenter_insecure`: `true` to skip TLS verification
|
||||||
- `settings.vcenter_event_polling_seconds`: event polling interval (0 disables)
|
- `settings.vcenter_event_polling_seconds`: deprecated and ignored
|
||||||
- `settings.vcenter_inventory_polling_seconds`: inventory polling interval (0 disables)
|
- `settings.vcenter_inventory_polling_seconds`: deprecated and ignored
|
||||||
- `settings.vcenter_inventory_snapshot_seconds`: hourly snapshot cadence (seconds)
|
- `settings.vcenter_inventory_snapshot_seconds`: hourly snapshot cadence (seconds)
|
||||||
- `settings.vcenter_inventory_aggregate_seconds`: daily aggregation cadence (seconds)
|
- `settings.vcenter_inventory_aggregate_seconds`: daily aggregation cadence (seconds)
|
||||||
- `settings.vcenter_addresses`: list of vCenter SDK URLs to monitor
|
- `settings.vcenter_addresses`: list of vCenter SDK URLs to monitor
|
||||||
|
|
||||||
|
Credential encryption:
|
||||||
|
- `VCTP_ENCRYPTION_KEY`: optional environment variable used to derive the encryption key.
|
||||||
|
If unset, vCTP derives a host key from hardware/host identity.
|
||||||
|
- New encrypted values are written with `enc:v1:` prefix.
|
||||||
|
|
||||||
Snapshots:
|
Snapshots:
|
||||||
- `settings.hourly_snapshot_concurrency`: max concurrent vCenter snapshots (0 = unlimited)
|
- `settings.hourly_snapshot_concurrency`: max concurrent vCenter snapshots (0 = unlimited)
|
||||||
- `settings.hourly_snapshot_max_age_days`: retention for hourly tables
|
- `settings.hourly_snapshot_max_age_days`: retention for hourly tables
|
||||||
|
|||||||
27
internal/secrets/secrets_test.go
Normal file
27
internal/secrets/secrets_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testLogger() *slog.Logger {
|
||||||
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptRejectsShortCiphertext(t *testing.T) {
|
||||||
|
key := []byte("0123456789abcdef0123456789abcdef")
|
||||||
|
s := New(testLogger(), key)
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString([]byte{1, 2, 3})
|
||||||
|
_, err := s.Decrypt(encoded)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for short ciphertext, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "ciphertext is too short") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateCerts(tlsCert string, tlsKey string) {
|
func GenerateCerts(tlsCert string, tlsKey string) error {
|
||||||
// @see https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
// @see https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
||||||
// @see https://golang.org/src/crypto/tls/generate_cert.go
|
// @see https://golang.org/src/crypto/tls/generate_cert.go
|
||||||
validFrom := ""
|
validFrom := ""
|
||||||
@@ -24,7 +25,7 @@ func GenerateCerts(tlsCert string, tlsKey string) {
|
|||||||
// Get the hostname
|
// Get the hostname
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return fmt.Errorf("failed to lookup hostname: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the directory exists
|
// Check that the directory exists
|
||||||
@@ -33,13 +34,15 @@ func GenerateCerts(tlsCert string, tlsKey string) {
|
|||||||
_, err = os.Stat(relativePath)
|
_, err = os.Stat(relativePath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
log.Printf("Certificate path does not exist, creating %s before generating certificate\n", relativePath)
|
log.Printf("Certificate path does not exist, creating %s before generating certificate\n", relativePath)
|
||||||
os.MkdirAll(relativePath, os.ModePerm)
|
if mkErr := os.MkdirAll(relativePath, os.ModePerm); mkErr != nil {
|
||||||
|
return fmt.Errorf("failed to create certificate directory %s: %w", relativePath, mkErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a private key
|
// Generate a private key
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
|
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to generate private key: %v", err)
|
return fmt.Errorf("failed to generate private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var notBefore time.Time
|
var notBefore time.Time
|
||||||
@@ -48,7 +51,7 @@ func GenerateCerts(tlsCert string, tlsKey string) {
|
|||||||
} else {
|
} else {
|
||||||
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
|
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse creation date: %v", err)
|
return fmt.Errorf("failed to parse creation date: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ func GenerateCerts(tlsCert string, tlsKey string) {
|
|||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to generate serial number: %v", err)
|
return fmt.Errorf("failed to generate serial number: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
template := x509.Certificate{
|
template := x509.Certificate{
|
||||||
@@ -105,35 +108,38 @@ func GenerateCerts(tlsCert string, tlsKey string) {
|
|||||||
|
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create certificate: %v", err)
|
return fmt.Errorf("failed to create certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
certOut, err := os.Create(tlsCert)
|
certOut, err := os.Create(tlsCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to open %s for writing: %v", tlsCert, err)
|
return fmt.Errorf("failed to open %s for writing: %w", tlsCert, err)
|
||||||
}
|
}
|
||||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||||
log.Fatalf("Failed to write data to %s: %v", tlsCert, err)
|
_ = certOut.Close()
|
||||||
|
return fmt.Errorf("failed to write certificate data to %s: %w", tlsCert, err)
|
||||||
}
|
}
|
||||||
if err := certOut.Close(); err != nil {
|
if err := certOut.Close(); err != nil {
|
||||||
log.Fatalf("Error closing %s: %v", tlsCert, err)
|
return fmt.Errorf("failed to close certificate file %s: %w", tlsCert, err)
|
||||||
}
|
}
|
||||||
log.Printf("wrote %s\n", tlsCert)
|
log.Printf("wrote %s\n", tlsCert)
|
||||||
|
|
||||||
keyOut, err := os.OpenFile(tlsKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
keyOut, err := os.OpenFile(tlsKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to open %s for writing: %v", tlsKey, err)
|
return fmt.Errorf("failed to open %s for writing: %w", tlsKey, err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to marshal private key: %v", err)
|
_ = keyOut.Close()
|
||||||
|
return fmt.Errorf("unable to marshal private key: %w", err)
|
||||||
}
|
}
|
||||||
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
||||||
log.Fatalf("Failed to write data to %s: %v", tlsKey, err)
|
_ = keyOut.Close()
|
||||||
|
return fmt.Errorf("failed to write private key data to %s: %w", tlsKey, err)
|
||||||
}
|
}
|
||||||
if err := keyOut.Close(); err != nil {
|
if err := keyOut.Close(); err != nil {
|
||||||
log.Fatalf("Error closing %s: %v", tlsKey, err)
|
return fmt.Errorf("failed to close private key file %s: %w", tlsKey, err)
|
||||||
}
|
}
|
||||||
log.Printf("wrote %s\n", tlsKey)
|
log.Printf("wrote %s\n", tlsKey)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
24
main.go
24
main.go
@@ -29,8 +29,6 @@ var (
|
|||||||
bindDisableTls bool
|
bindDisableTls bool
|
||||||
sha1ver string // sha1 revision used to build the program
|
sha1ver string // sha1 revision used to build the program
|
||||||
buildTime string // when the executable was built
|
buildTime string // when the executable was built
|
||||||
cronFrequency time.Duration
|
|
||||||
cronInvFrequency time.Duration
|
|
||||||
cronSnapshotFrequency time.Duration
|
cronSnapshotFrequency time.Duration
|
||||||
cronAggregateFrequency time.Duration
|
cronAggregateFrequency time.Duration
|
||||||
)
|
)
|
||||||
@@ -66,6 +64,7 @@ func main() {
|
|||||||
s.Logger = logger
|
s.Logger = logger
|
||||||
|
|
||||||
logger.Info("vCTP starting", "build_time", buildTime, "sha1_version", sha1ver, "go_version", runtime.Version(), "settings_file", *settingsPath)
|
logger.Info("vCTP starting", "build_time", buildTime, "sha1_version", sha1ver, "go_version", runtime.Version(), "settings_file", *settingsPath)
|
||||||
|
warnDeprecatedPollingSettings(logger, s.Values)
|
||||||
|
|
||||||
// Configure database
|
// Configure database
|
||||||
dbDriver := strings.TrimSpace(s.Values.Settings.DatabaseDriver)
|
dbDriver := strings.TrimSpace(s.Values.Settings.DatabaseDriver)
|
||||||
@@ -137,7 +136,10 @@ func main() {
|
|||||||
// Generate certificate if required
|
// Generate certificate if required
|
||||||
if !(utils.FileExists(tlsCertFilename) && utils.FileExists(tlsKeyFilename)) {
|
if !(utils.FileExists(tlsCertFilename) && utils.FileExists(tlsKeyFilename)) {
|
||||||
logger.Warn("Specified TLS certificate or private key do not exist", "certificate", tlsCertFilename, "tls-key", tlsKeyFilename)
|
logger.Warn("Specified TLS certificate or private key do not exist", "certificate", tlsCertFilename, "tls-key", tlsKeyFilename)
|
||||||
utils.GenerateCerts(tlsCertFilename, tlsKeyFilename)
|
if err := utils.GenerateCerts(tlsCertFilename, tlsKeyFilename); err != nil {
|
||||||
|
logger.Error("failed to generate TLS cert/key", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load vcenter credentials from settings, decrypt if required.
|
// Load vcenter credentials from settings, decrypt if required.
|
||||||
@@ -338,6 +340,22 @@ func alignStart(now time.Time, freq time.Duration) time.Time {
|
|||||||
return now.Add(freq)
|
return now.Add(freq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func warnDeprecatedPollingSettings(logger *slog.Logger, cfg *settings.SettingsYML) {
|
||||||
|
if cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.Settings.VcenterEventPollingSeconds > 0 {
|
||||||
|
logger.Warn("vcenter_event_polling_seconds is deprecated and ignored; snapshot lifecycle processing is used instead",
|
||||||
|
"value", cfg.Settings.VcenterEventPollingSeconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if cfg.Settings.VcenterInventoryPollingSeconds > 0 {
|
||||||
|
logger.Warn("vcenter_inventory_polling_seconds is deprecated and ignored; hourly snapshot jobs are used instead",
|
||||||
|
"value", cfg.Settings.VcenterInventoryPollingSeconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func durationFromSeconds(value int, fallback int) time.Duration {
|
func durationFromSeconds(value int, fallback int) time.Duration {
|
||||||
if value <= 0 {
|
if value <= 0 {
|
||||||
return time.Second * time.Duration(fallback)
|
return time.Second * time.Duration(fallback)
|
||||||
|
|||||||
112
main_test.go
Normal file
112
main_test.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"vctp/internal/secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testLogger() *slog.Logger {
|
||||||
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustEncrypt(t *testing.T, s *secrets.Secrets, plain string) string {
|
||||||
|
t.Helper()
|
||||||
|
enc, err := s.Encrypt([]byte(plain))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt failed: %v", err)
|
||||||
|
}
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveVcenterPasswordPlaintextRewrite(t *testing.T) {
|
||||||
|
logger := testLogger()
|
||||||
|
key := []byte("0123456789abcdef0123456789abcdef")
|
||||||
|
cipher := secrets.New(logger, key)
|
||||||
|
|
||||||
|
pass, rewritten, err := resolveVcenterPassword(logger, cipher, nil, "my-password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(pass) != "my-password" {
|
||||||
|
t.Fatalf("unexpected plaintext returned: %q", string(pass))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(rewritten, encryptedVcenterPasswordPrefix) {
|
||||||
|
t.Fatalf("expected rewritten prefixed credential, got: %q", rewritten)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveVcenterPasswordUnprefixedLegacyCiphertextRewrite(t *testing.T) {
|
||||||
|
logger := testLogger()
|
||||||
|
activeKey := []byte("0123456789abcdef0123456789abcdef")
|
||||||
|
legacyKey := []byte("abcdef0123456789abcdef0123456789")
|
||||||
|
activeCipher := secrets.New(logger, activeKey)
|
||||||
|
legacyCipher := secrets.New(logger, legacyKey)
|
||||||
|
|
||||||
|
legacyCiphertext := mustEncrypt(t, legacyCipher, "legacy-secret")
|
||||||
|
|
||||||
|
pass, rewritten, err := resolveVcenterPassword(logger, activeCipher, [][]byte{legacyKey}, legacyCiphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(pass) != "legacy-secret" {
|
||||||
|
t.Fatalf("unexpected plaintext returned: %q", string(pass))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(rewritten, encryptedVcenterPasswordPrefix) {
|
||||||
|
t.Fatalf("expected rewritten prefixed credential, got: %q", rewritten)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := activeCipher.Decrypt(strings.TrimPrefix(rewritten, encryptedVcenterPasswordPrefix))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rewritten ciphertext did not decrypt with active key: %v", err)
|
||||||
|
}
|
||||||
|
if string(decoded) != "legacy-secret" {
|
||||||
|
t.Fatalf("unexpected rewritten decrypt value: %q", string(decoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveVcenterPasswordPrefixedLegacyCiphertextRewrite(t *testing.T) {
|
||||||
|
logger := testLogger()
|
||||||
|
activeKey := []byte("0123456789abcdef0123456789abcdef")
|
||||||
|
legacyKey := []byte("abcdef0123456789abcdef0123456789")
|
||||||
|
activeCipher := secrets.New(logger, activeKey)
|
||||||
|
legacyCipher := secrets.New(logger, legacyKey)
|
||||||
|
|
||||||
|
legacyCiphertext := mustEncrypt(t, legacyCipher, "legacy-prefixed-secret")
|
||||||
|
raw := encryptedVcenterPasswordPrefix + legacyCiphertext
|
||||||
|
|
||||||
|
pass, rewritten, err := resolveVcenterPassword(logger, activeCipher, [][]byte{legacyKey}, raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(pass) != "legacy-prefixed-secret" {
|
||||||
|
t.Fatalf("unexpected plaintext returned: %q", string(pass))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(rewritten, encryptedVcenterPasswordPrefix) {
|
||||||
|
t.Fatalf("expected rewritten prefixed credential, got: %q", rewritten)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := activeCipher.Decrypt(strings.TrimPrefix(rewritten, encryptedVcenterPasswordPrefix))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rewritten ciphertext did not decrypt with active key: %v", err)
|
||||||
|
}
|
||||||
|
if string(decoded) != "legacy-prefixed-secret" {
|
||||||
|
t.Fatalf("unexpected rewritten decrypt value: %q", string(decoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveVcenterPasswordShortPlaintextRejected(t *testing.T) {
|
||||||
|
logger := testLogger()
|
||||||
|
key := []byte("0123456789abcdef0123456789abcdef")
|
||||||
|
cipher := secrets.New(logger, key)
|
||||||
|
|
||||||
|
_, _, err := resolveVcenterPassword(logger, cipher, nil, "ab")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected short plaintext error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "too short") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,18 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const encryptedValuePrefixV1 = "enc:v1:"
|
||||||
|
|
||||||
|
type encryptRequest struct {
|
||||||
|
Plaintext string `json:"plaintext"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptData encrypts a plaintext value and returns the ciphertext.
|
// EncryptData encrypts a plaintext value and returns the ciphertext.
|
||||||
// @Summary Encrypt data
|
// @Summary Encrypt data
|
||||||
// @Description Encrypts a plaintext value and returns the ciphertext.
|
// @Description Encrypts a plaintext value and returns the ciphertext.
|
||||||
@@ -15,57 +22,47 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body map[string]string true "Plaintext payload"
|
// @Param payload body map[string]string true "Plaintext payload"
|
||||||
// @Success 200 {object} models.StatusMessageResponse "Ciphertext response"
|
// @Success 200 {object} models.StatusMessageResponse "Ciphertext response"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Invalid request"
|
||||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||||
// @Router /api/encrypt [post]
|
// @Router /api/encrypt [post]
|
||||||
func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) {
|
||||||
//ctx := context.Background()
|
if r.Method != http.MethodPost {
|
||||||
var cipherText string
|
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
reqBody, err := io.ReadAll(r.Body)
|
var req encryptRequest
|
||||||
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil {
|
||||||
|
h.Logger.Error("unable to decode encrypt request", "error", err)
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid JSON body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plaintext := strings.TrimSpace(req.Plaintext)
|
||||||
|
if plaintext == "" {
|
||||||
|
plaintext = strings.TrimSpace(req.Value)
|
||||||
|
}
|
||||||
|
if plaintext == "" {
|
||||||
|
plaintext = strings.TrimSpace(req.Message)
|
||||||
|
}
|
||||||
|
if plaintext == "" {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "plaintext is required (accepted keys: plaintext, value, message)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText, err := h.Secret.Encrypt([]byte(plaintext))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Logger.Error("Invalid data received", "error", err)
|
h.Logger.Error("unable to encrypt payload", "error", err)
|
||||||
fmt.Fprintf(w, "Invalid data received")
|
writeJSONError(w, http.StatusInternalServerError, "encryption failed")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
h.Logger.Debug("received input data", "length", len(reqBody))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the json input
|
h.Logger.Debug("encrypted plaintext payload", "input_length", len(plaintext))
|
||||||
var input map[string]string
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if err := json.Unmarshal(reqBody, &input); err != nil {
|
w.WriteHeader(http.StatusOK)
|
||||||
h.Logger.Error("unable to unmarshal json", "error", err)
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||||
prettyPrint(reqBody)
|
"status": "OK",
|
||||||
w.Header().Set("Content-Type", "application/json")
|
"message": cipherText,
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
"prefixed": encryptedValuePrefixV1 + cipherText,
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
"ciphertext": cipherText,
|
||||||
"status": "ERROR",
|
})
|
||||||
"message": fmt.Sprintf("Unable to unmarshal JSON in request body: '%s'", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
h.Logger.Debug("successfully decoded JSON")
|
|
||||||
//prettyPrint(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
//cipher, err := h.Secret.Encrypt()
|
|
||||||
for k := range input {
|
|
||||||
//h.Logger.Debug("foo", "key", k, "value", input[k])
|
|
||||||
cipherText, err = h.Secret.Encrypt([]byte(input[k]))
|
|
||||||
if err != nil {
|
|
||||||
h.Logger.Error("Unable to encrypt", "error", err)
|
|
||||||
} else {
|
|
||||||
h.Logger.Debug("Encrypted plaintext", "length", len(input[k]), "ciphertext", cipherText)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"status": "OK",
|
|
||||||
"message": cipherText,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the result
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
server/handler/legacy_gate.go
Normal file
23
server/handler/legacy_gate.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const legacyAPIEnvVar = "VCTP_ENABLE_LEGACY_API"
|
||||||
|
|
||||||
|
func legacyAPIEnabled() bool {
|
||||||
|
return strings.TrimSpace(os.Getenv(legacyAPIEnvVar)) == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) denyLegacyAPI(w http.ResponseWriter, endpoint string) bool {
|
||||||
|
if legacyAPIEnabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
h.Logger.Warn("legacy endpoint request blocked", "endpoint", endpoint, "env_var", legacyAPIEnvVar)
|
||||||
|
writeJSONError(w, http.StatusGone, fmt.Sprintf("%s is deprecated and disabled; set %s=1 to temporarily re-enable", endpoint, legacyAPIEnvVar))
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@ import (
|
|||||||
// @Failure 500 {string} string "Server error"
|
// @Failure 500 {string} string "Server error"
|
||||||
// @Router /api/cleanup/updates [delete]
|
// @Router /api/cleanup/updates [delete]
|
||||||
func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.denyLegacyAPI(w, "/api/cleanup/updates") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Get the current time
|
// Get the current time
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import (
|
|||||||
// @Failure 400 {object} models.ErrorResponse "Invalid request"
|
// @Failure 400 {object} models.ErrorResponse "Invalid request"
|
||||||
// @Router /api/cleanup/vcenter [delete]
|
// @Router /api/cleanup/vcenter [delete]
|
||||||
func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.denyLegacyAPI(w, "/api/cleanup/vcenter") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Get the parameters
|
// Get the parameters
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import (
|
|||||||
// @Failure 500 {string} string "Server error"
|
// @Failure 500 {string} string "Server error"
|
||||||
// @Router /api/event/vm/create [post]
|
// @Router /api/event/vm/create [post]
|
||||||
func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.denyLegacyAPI(w, "/api/event/vm/create") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
unixTimestamp int64
|
unixTimestamp int64
|
||||||
//numVcpus int32
|
//numVcpus int32
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import (
|
|||||||
// @Failure 500 {string} string "Server error"
|
// @Failure 500 {string} string "Server error"
|
||||||
// @Router /api/event/vm/delete [post]
|
// @Router /api/event/vm/delete [post]
|
||||||
func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.denyLegacyAPI(w, "/api/event/vm/delete") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
deletedTimestamp int64
|
deletedTimestamp int64
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ import (
|
|||||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||||
// @Router /api/event/vm/modify [post]
|
// @Router /api/event/vm/modify [post]
|
||||||
func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.denyLegacyAPI(w, "/api/event/vm/modify") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var configChanges []map[string]string
|
var configChanges []map[string]string
|
||||||
params := queries.CreateUpdateParams{}
|
params := queries.CreateUpdateParams{}
|
||||||
var unixTimestamp int64
|
var unixTimestamp int64
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import (
|
|||||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||||
// @Router /api/event/vm/move [post]
|
// @Router /api/event/vm/move [post]
|
||||||
func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.denyLegacyAPI(w, "/api/event/vm/move") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
params := queries.CreateUpdateParams{}
|
params := queries.CreateUpdateParams{}
|
||||||
var unixTimestamp int64
|
var unixTimestamp int64
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
|||||||
// add missing data to VMs
|
// add missing data to VMs
|
||||||
//mux.HandleFunc("/api/inventory/vm/update", h.VmUpdateDetails)
|
//mux.HandleFunc("/api/inventory/vm/update", h.VmUpdateDetails)
|
||||||
|
|
||||||
// temporary endpoint
|
// Legacy/maintenance endpoints are gated by VCTP_ENABLE_LEGACY_API.
|
||||||
mux.HandleFunc("/api/cleanup/updates", h.UpdateCleanup)
|
mux.HandleFunc("/api/cleanup/updates", h.UpdateCleanup)
|
||||||
//mux.HandleFunc("/api/cleanup/vcenter", h.VcCleanup)
|
//mux.HandleFunc("/api/cleanup/vcenter", h.VcCleanup)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
CPE_OPTS='-settings /etc/dtms/vctp.yml'
|
CPE_OPTS='-settings /etc/dtms/vctp.yml'
|
||||||
MONTHLY_AGG_GO=0
|
MONTHLY_AGG_GO=0
|
||||||
DAILY_AGG_GO=0
|
DAILY_AGG_GO=0
|
||||||
|
# Optional explicit encryption key source (recommended for stable credential decryption across host changes):
|
||||||
|
# VCTP_ENCRYPTION_KEY=''
|
||||||
|
# PostgreSQL is experimental and disabled by default:
|
||||||
|
# VCTP_ENABLE_EXPERIMENTAL_POSTGRES=0
|
||||||
|
# Deprecated API endpoints are disabled by default:
|
||||||
|
# VCTP_ENABLE_LEGACY_API=0
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ settings:
|
|||||||
vcenter_username: ""
|
vcenter_username: ""
|
||||||
vcenter_password: ""
|
vcenter_password: ""
|
||||||
vcenter_insecure: false
|
vcenter_insecure: false
|
||||||
vcenter_event_polling_seconds: 60
|
# Deprecated (ignored): legacy event poller
|
||||||
vcenter_inventory_polling_seconds: 7200
|
vcenter_event_polling_seconds: 0
|
||||||
|
# Deprecated (ignored): legacy inventory poller
|
||||||
|
vcenter_inventory_polling_seconds: 0
|
||||||
vcenter_inventory_snapshot_seconds: 3600
|
vcenter_inventory_snapshot_seconds: 3600
|
||||||
vcenter_inventory_aggregate_seconds: 86400
|
vcenter_inventory_aggregate_seconds: 86400
|
||||||
hourly_snapshot_concurrency: 0
|
hourly_snapshot_concurrency: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user