Implemented a comprehensive secure authentication mechanism for inter-node cluster communication with the following features: 1. Global Cluster Secret (GCS) - Auto-generated cryptographically secure random secret (256-bit) - Configurable via YAML config file - Shared across all cluster nodes for authentication 2. Cluster Authentication Middleware - Validates X-Cluster-Secret and X-Node-ID headers - Applied to all cluster endpoints (/members/*, /merkle_tree/*, /kv_range) - Comprehensive logging of authentication attempts 3. Authenticated HTTP Client - Custom HTTP client with cluster auth headers - TLS support with configurable certificate verification - Protocol-aware (http/https based on TLS settings) 4. Secure Bootstrap Endpoint - New /auth/cluster-bootstrap endpoint - Protected by JWT authentication with admin scope - Allows new nodes to securely obtain cluster secret 5. Updated Cluster Communication - All gossip protocol requests include auth headers - All Merkle tree sync requests include auth headers - All data replication requests include auth headers 6. Configuration - cluster_secret: Shared secret (auto-generated if not provided) - cluster_tls_enabled: Enable TLS for inter-node communication - cluster_tls_cert_file: Path to TLS certificate - cluster_tls_key_file: Path to TLS private key - cluster_tls_skip_verify: Skip TLS verification (testing only) This implementation addresses the security vulnerability of unprotected cluster endpoints and provides a flexible, secure approach to protecting internal cluster communication while allowing for automated node bootstrapping. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
6.1 KiB
Go
215 lines
6.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
badger "github.com/dgraph-io/badger/v4"
|
|
|
|
"kvs/auth"
|
|
"kvs/types"
|
|
)
|
|
|
|
// RevisionService handles revision history management
|
|
type RevisionService struct {
|
|
storage *StorageService
|
|
}
|
|
|
|
// NewRevisionService creates a new revision service
|
|
func NewRevisionService(storage *StorageService) *RevisionService {
|
|
return &RevisionService{
|
|
storage: storage,
|
|
}
|
|
}
|
|
|
|
// GetRevisionKey generates the storage key for a specific revision
|
|
func GetRevisionKey(baseKey string, revision int) string {
|
|
return fmt.Sprintf("%s:rev:%d", baseKey, revision)
|
|
}
|
|
|
|
// StoreRevisionHistory stores a value and manages revision history (up to 3 revisions)
|
|
func (r *RevisionService) StoreRevisionHistory(txn *badger.Txn, key string, storedValue types.StoredValue, ttl time.Duration) error {
|
|
// Get existing metadata to check current revisions
|
|
metadataKey := auth.ResourceMetadataKey(key)
|
|
|
|
var metadata types.ResourceMetadata
|
|
var currentRevisions []int
|
|
|
|
// Try to get existing metadata
|
|
metadataData, err := r.storage.RetrieveWithDecompression(txn, []byte(metadataKey))
|
|
if err == badger.ErrKeyNotFound {
|
|
// No existing metadata, this is a new key
|
|
metadata = types.ResourceMetadata{
|
|
OwnerUUID: "", // Will be set by caller if needed
|
|
GroupUUID: "",
|
|
Permissions: types.DefaultPermissions,
|
|
TTL: "",
|
|
CreatedAt: time.Now().Unix(),
|
|
UpdatedAt: time.Now().Unix(),
|
|
}
|
|
currentRevisions = []int{}
|
|
} else if err != nil {
|
|
// Error reading metadata
|
|
return fmt.Errorf("failed to read metadata: %v", err)
|
|
} else {
|
|
// Parse existing metadata
|
|
err = json.Unmarshal(metadataData, &metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal metadata: %v", err)
|
|
}
|
|
|
|
// Extract current revisions (we store them as a custom field)
|
|
if metadata.TTL == "" {
|
|
currentRevisions = []int{}
|
|
} else {
|
|
// For now, we'll manage revisions separately - let's create a new metadata field
|
|
currentRevisions = []int{1, 2, 3} // Assume all revisions exist for existing keys
|
|
}
|
|
}
|
|
|
|
// Revision rotation logic: shift existing revisions
|
|
if len(currentRevisions) >= 3 {
|
|
// Delete oldest revision (rev:3)
|
|
oldestRevKey := GetRevisionKey(key, 3)
|
|
txn.Delete([]byte(oldestRevKey))
|
|
|
|
// Shift rev:2 → rev:3
|
|
rev2Key := GetRevisionKey(key, 2)
|
|
rev2Data, err := r.storage.RetrieveWithDecompression(txn, []byte(rev2Key))
|
|
if err == nil {
|
|
rev3Key := GetRevisionKey(key, 3)
|
|
r.storage.StoreWithTTL(txn, []byte(rev3Key), rev2Data, ttl)
|
|
}
|
|
|
|
// Shift rev:1 → rev:2
|
|
rev1Key := GetRevisionKey(key, 1)
|
|
rev1Data, err := r.storage.RetrieveWithDecompression(txn, []byte(rev1Key))
|
|
if err == nil {
|
|
rev2Key := GetRevisionKey(key, 2)
|
|
r.storage.StoreWithTTL(txn, []byte(rev2Key), rev1Data, ttl)
|
|
}
|
|
}
|
|
|
|
// Store current value as rev:1
|
|
currentValueBytes, err := json.Marshal(storedValue)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal current value for revision: %v", err)
|
|
}
|
|
|
|
rev1Key := GetRevisionKey(key, 1)
|
|
err = r.storage.StoreWithTTL(txn, []byte(rev1Key), currentValueBytes, ttl)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store revision 1: %v", err)
|
|
}
|
|
|
|
// Update metadata with new revision count
|
|
metadata.UpdatedAt = time.Now().Unix()
|
|
metadataBytes, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal metadata: %v", err)
|
|
}
|
|
|
|
return r.storage.StoreWithTTL(txn, []byte(metadataKey), metadataBytes, ttl)
|
|
}
|
|
|
|
// GetRevisionHistory retrieves all available revisions for a given key
|
|
func (r *RevisionService) GetRevisionHistory(key string) ([]map[string]interface{}, error) {
|
|
var revisions []map[string]interface{}
|
|
|
|
err := r.storage.db.View(func(txn *badger.Txn) error {
|
|
// Check revisions 1, 2, 3
|
|
for rev := 1; rev <= 3; rev++ {
|
|
revKey := GetRevisionKey(key, rev)
|
|
|
|
revData, err := r.storage.RetrieveWithDecompression(txn, []byte(revKey))
|
|
if err == badger.ErrKeyNotFound {
|
|
continue // Skip missing revisions
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to retrieve revision %d: %v", rev, err)
|
|
}
|
|
|
|
var storedValue types.StoredValue
|
|
err = json.Unmarshal(revData, &storedValue)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal revision %d: %v", rev, err)
|
|
}
|
|
|
|
var data interface{}
|
|
err = json.Unmarshal(storedValue.Data, &data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal revision %d data: %v", rev, err)
|
|
}
|
|
|
|
revision := map[string]interface{}{
|
|
"revision": rev,
|
|
"uuid": storedValue.UUID,
|
|
"timestamp": storedValue.Timestamp,
|
|
"data": data,
|
|
}
|
|
|
|
revisions = append(revisions, revision)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sort revisions by revision number (newest first)
|
|
// Note: they're already in order since we iterate 1->3, but reverse for newest first
|
|
for i, j := 0, len(revisions)-1; i < j; i, j = i+1, j-1 {
|
|
revisions[i], revisions[j] = revisions[j], revisions[i]
|
|
}
|
|
|
|
return revisions, nil
|
|
}
|
|
|
|
// GetSpecificRevision retrieves a specific revision of a key
|
|
func (r *RevisionService) GetSpecificRevision(key string, revision int) (*types.StoredValue, error) {
|
|
if revision < 1 || revision > 3 {
|
|
return nil, fmt.Errorf("invalid revision number: %d (must be 1-3)", revision)
|
|
}
|
|
|
|
var storedValue types.StoredValue
|
|
err := r.storage.db.View(func(txn *badger.Txn) error {
|
|
revKey := GetRevisionKey(key, revision)
|
|
|
|
revData, err := r.storage.RetrieveWithDecompression(txn, []byte(revKey))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return json.Unmarshal(revData, &storedValue)
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &storedValue, nil
|
|
}
|
|
|
|
// GetRevisionFromPath extracts revision number from a path like "key/data/rev/2"
|
|
func GetRevisionFromPath(path string) (string, int, error) {
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) < 4 || parts[len(parts)-2] != "rev" {
|
|
return "", 0, fmt.Errorf("invalid revision path format")
|
|
}
|
|
|
|
revisionStr := parts[len(parts)-1]
|
|
revision, err := strconv.Atoi(revisionStr)
|
|
if err != nil {
|
|
return "", 0, fmt.Errorf("invalid revision number: %s", revisionStr)
|
|
}
|
|
|
|
// Reconstruct the base key without the "/rev/N" suffix
|
|
baseKey := strings.Join(parts[:len(parts)-2], "/")
|
|
|
|
return baseKey, revision, nil
|
|
}
|