diff --git a/auth/auth.go b/auth/auth.go index 00fa831..7db98b0 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -41,7 +41,7 @@ func NewAuthService(db *badger.DB, logger *logrus.Logger, config *types.Config) // StoreAPIToken stores an API token in BadgerDB with TTL func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes []string, expiresAt int64) error { tokenHash := utils.HashToken(tokenString) - + apiToken := types.APIToken{ TokenHash: tokenHash, UserUUID: userUUID, @@ -57,13 +57,13 @@ func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes return s.db.Update(func(txn *badger.Txn) error { entry := badger.NewEntry([]byte(TokenStorageKey(tokenHash)), tokenData) - + // Set TTL to the token expiration time ttl := time.Until(time.Unix(expiresAt, 0)) if ttl > 0 { entry = entry.WithTTL(ttl) } - + return txn.SetEntry(entry) }) } @@ -71,7 +71,7 @@ func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes // GetAPIToken retrieves an API token from BadgerDB by hash func (s *AuthService) GetAPIToken(tokenHash string) (*types.APIToken, error) { var apiToken types.APIToken - + err := s.db.View(func(txn *badger.Txn) error { item, err := txn.Get([]byte(TokenStorageKey(tokenHash))) if err != nil { @@ -209,22 +209,22 @@ func GetAuthContext(ctx context.Context) *AuthContext { // HasUsers checks if any users exist in the database func (s *AuthService) HasUsers() (bool, error) { var hasUsers bool - + err := s.db.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions opts.PrefetchValues = false // We only need to check if keys exist iterator := txn.NewIterator(opts) defer iterator.Close() - + // Look for any key starting with "user:" prefix := []byte("user:") for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() { hasUsers = true return nil // Found at least one user, can exit early } - + return nil }) - + return hasUsers, err -} \ No newline at end of file +} diff --git a/auth/cluster.go b/auth/cluster.go new file mode 100644 index 0000000..090ed5d --- /dev/null +++ b/auth/cluster.go @@ -0,0 +1,77 @@ +package auth + +import ( + "net/http" + + "github.com/sirupsen/logrus" +) + +// ClusterAuthService handles authentication for inter-cluster communication +type ClusterAuthService struct { + clusterSecret string + logger *logrus.Logger +} + +// NewClusterAuthService creates a new cluster authentication service +func NewClusterAuthService(clusterSecret string, logger *logrus.Logger) *ClusterAuthService { + return &ClusterAuthService{ + clusterSecret: clusterSecret, + logger: logger, + } +} + +// Middleware validates cluster authentication headers +func (s *ClusterAuthService) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Extract authentication headers + clusterSecret := r.Header.Get("X-Cluster-Secret") + nodeID := r.Header.Get("X-Node-ID") + + // Log authentication attempt + s.logger.WithFields(logrus.Fields{ + "node_id": nodeID, + "remote_addr": r.RemoteAddr, + "path": r.URL.Path, + "method": r.Method, + }).Debug("Cluster authentication attempt") + + // Validate cluster secret + if clusterSecret == "" { + s.logger.WithFields(logrus.Fields{ + "node_id": nodeID, + "remote_addr": r.RemoteAddr, + "path": r.URL.Path, + }).Warn("Missing X-Cluster-Secret header") + http.Error(w, "Unauthorized: Missing cluster secret", http.StatusUnauthorized) + return + } + + if clusterSecret != s.clusterSecret { + s.logger.WithFields(logrus.Fields{ + "node_id": nodeID, + "remote_addr": r.RemoteAddr, + "path": r.URL.Path, + }).Warn("Invalid cluster secret") + http.Error(w, "Unauthorized: Invalid cluster secret", http.StatusUnauthorized) + return + } + + // Validate node ID is present + if nodeID == "" { + s.logger.WithFields(logrus.Fields{ + "remote_addr": r.RemoteAddr, + "path": r.URL.Path, + }).Warn("Missing X-Node-ID header") + http.Error(w, "Unauthorized: Missing node ID", http.StatusUnauthorized) + return + } + + // Authentication successful + s.logger.WithFields(logrus.Fields{ + "node_id": nodeID, + "path": r.URL.Path, + }).Debug("Cluster authentication successful") + + next.ServeHTTP(w, r) + }) +} diff --git a/auth/jwt.go b/auth/jwt.go index eda4f3a..5b3e781 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -64,4 +64,4 @@ func ValidateJWT(tokenString string) (*JWTClaims, error) { } return nil, fmt.Errorf("invalid token") -} \ No newline at end of file +} diff --git a/auth/middleware.go b/auth/middleware.go index 07c244e..82b348e 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -33,7 +33,7 @@ func (s *AuthService) Middleware(requiredScopes []string, resourceKeyExtractor f next(w, r) return } - + // Authenticate request authCtx, err := s.AuthenticateRequest(r) if err != nil { @@ -102,7 +102,7 @@ func (s *RateLimitService) RateLimitMiddleware(next http.HandlerFunc) http.Handl next(w, r) return } - + // Extract auth context to get user UUID authCtx := GetAuthContext(r.Context()) if authCtx == nil { @@ -110,7 +110,7 @@ func (s *RateLimitService) RateLimitMiddleware(next http.HandlerFunc) http.Handl next(w, r) return } - + // Check rate limit allowed, err := s.checkRateLimit(authCtx.UserUUID) if err != nil { @@ -118,22 +118,22 @@ func (s *RateLimitService) RateLimitMiddleware(next http.HandlerFunc) http.Handl http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - + if !allowed { s.authService.logger.WithFields(logrus.Fields{ "user_uuid": authCtx.UserUUID, "limit": s.config.RateLimitRequests, "window": s.config.RateLimitWindow, }).Info("Rate limit exceeded") - + // Set rate limit headers w.Header().Set("X-Rate-Limit-Limit", strconv.Itoa(s.config.RateLimitRequests)) w.Header().Set("X-Rate-Limit-Window", s.config.RateLimitWindow) - + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) return } - + next(w, r) } } @@ -151,8 +151,8 @@ func (s *RateLimitService) checkRateLimit(userUUID string) (bool, error) { if s.config.RateLimitRequests <= 0 { return true, nil // Rate limiting disabled } - + // Simplified rate limiting - in practice this would use the full implementation // that was in main.go with proper window calculations and BadgerDB storage return true, nil // For now, always allow -} \ No newline at end of file +} diff --git a/auth/permissions.go b/auth/permissions.go index 7130da1..9e39fc3 100644 --- a/auth/permissions.go +++ b/auth/permissions.go @@ -15,7 +15,7 @@ func CheckPermission(permissions int, operation string, isOwner, isGroupMember b return (permissions & types.PermGroupCreate) != 0 } return (permissions & types.PermOthersCreate) != 0 - + case "delete": if isOwner { return (permissions & types.PermOwnerDelete) != 0 @@ -24,7 +24,7 @@ func CheckPermission(permissions int, operation string, isOwner, isGroupMember b return (permissions & types.PermGroupDelete) != 0 } return (permissions & types.PermOthersDelete) != 0 - + case "write": if isOwner { return (permissions & types.PermOwnerWrite) != 0 @@ -33,7 +33,7 @@ func CheckPermission(permissions int, operation string, isOwner, isGroupMember b return (permissions & types.PermGroupWrite) != 0 } return (permissions & types.PermOthersWrite) != 0 - + case "read": if isOwner { return (permissions & types.PermOwnerRead) != 0 @@ -42,7 +42,7 @@ func CheckPermission(permissions int, operation string, isOwner, isGroupMember b return (permissions & types.PermGroupRead) != 0 } return (permissions & types.PermOthersRead) != 0 - + default: return false } @@ -51,7 +51,7 @@ func CheckPermission(permissions int, operation string, isOwner, isGroupMember b // CheckUserResourceRelationship determines user relationship to resource func CheckUserResourceRelationship(userUUID string, metadata *types.ResourceMetadata, userGroups []string) (isOwner, isGroupMember bool) { isOwner = (userUUID == metadata.OwnerUUID) - + if metadata.GroupUUID != "" { for _, groupUUID := range userGroups { if groupUUID == metadata.GroupUUID { @@ -60,6 +60,6 @@ func CheckUserResourceRelationship(userUUID string, metadata *types.ResourceMeta } } } - + return isOwner, isGroupMember -} \ No newline at end of file +} diff --git a/auth/storage.go b/auth/storage.go index bccf16e..889cca1 100644 --- a/auth/storage.go +++ b/auth/storage.go @@ -16,4 +16,4 @@ func TokenStorageKey(tokenHash string) string { func ResourceMetadataKey(resourceKey string) string { return resourceKey + ":metadata" -} \ No newline at end of file +} diff --git a/cluster/bootstrap.go b/cluster/bootstrap.go index f8e4704..18c5e09 100644 --- a/cluster/bootstrap.go +++ b/cluster/bootstrap.go @@ -142,4 +142,4 @@ func (s *BootstrapService) performGradualSync() { } s.logger.Info("Gradual sync completed") -} \ No newline at end of file +} diff --git a/cluster/gossip.go b/cluster/gossip.go index cbdf74a..f58683f 100644 --- a/cluster/gossip.go +++ b/cluster/gossip.go @@ -17,13 +17,13 @@ import ( // GossipService handles gossip protocol operations type GossipService struct { - config *types.Config - members map[string]*types.Member - membersMu sync.RWMutex - logger *logrus.Logger - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup + config *types.Config + members map[string]*types.Member + membersMu sync.RWMutex + logger *logrus.Logger + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup } // NewGossipService creates a new gossip service @@ -44,7 +44,7 @@ func (s *GossipService) Start() { s.logger.Info("Clustering disabled, skipping gossip routine") return } - + s.wg.Add(1) go s.gossipRoutine() } @@ -181,11 +181,20 @@ func (s *GossipService) gossipWithPeer(peer *types.Member) error { return err } - // Send HTTP request to peer - client := &http.Client{Timeout: 5 * time.Second} - url := fmt.Sprintf("http://%s/members/gossip", peer.Address) + // Send HTTP request to peer with cluster authentication + client := NewAuthenticatedHTTPClient(s.config, 5*time.Second) + protocol := GetProtocol(s.config) + url := fmt.Sprintf("%s://%s/members/gossip", protocol, peer.Address) - resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + s.logger.WithError(err).Error("Failed to create gossip request") + return err + } + req.Header.Set("Content-Type", "application/json") + AddClusterAuthHeaders(req, s.config) + + resp, err := client.Do(req) if err != nil { s.logger.WithFields(logrus.Fields{ "peer": peer.Address, @@ -300,4 +309,4 @@ func (s *GossipService) MergeMemberList(remoteMembers []types.Member, selfNodeID func (s *GossipService) GetJoinedTimestamp() int64 { // This should be implemented by the server that uses this service return time.Now().UnixMilli() -} \ No newline at end of file +} diff --git a/cluster/http_client.go b/cluster/http_client.go new file mode 100644 index 0000000..70397b6 --- /dev/null +++ b/cluster/http_client.go @@ -0,0 +1,43 @@ +package cluster + +import ( + "crypto/tls" + "net/http" + "time" + + "kvs/types" +) + +// NewAuthenticatedHTTPClient creates an HTTP client configured for cluster authentication +func NewAuthenticatedHTTPClient(config *types.Config, timeout time.Duration) *http.Client { + client := &http.Client{ + Timeout: timeout, + } + + // Configure TLS if enabled + if config.ClusterTLSEnabled { + tlsConfig := &tls.Config{ + InsecureSkipVerify: config.ClusterTLSSkipVerify, + } + + client.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + } + + return client +} + +// AddClusterAuthHeaders adds authentication headers to an HTTP request +func AddClusterAuthHeaders(req *http.Request, config *types.Config) { + req.Header.Set("X-Cluster-Secret", config.ClusterSecret) + req.Header.Set("X-Node-ID", config.NodeID) +} + +// GetProtocol returns the appropriate protocol (http or https) based on TLS configuration +func GetProtocol(config *types.Config) string { + if config.ClusterTLSEnabled { + return "https" + } + return "http" +} diff --git a/cluster/merkle.go b/cluster/merkle.go index e6032fe..df84486 100644 --- a/cluster/merkle.go +++ b/cluster/merkle.go @@ -170,7 +170,7 @@ func (s *MerkleService) BuildSubtreeForRange(startKey, endKey string) (*types.Me if err != nil { return nil, fmt.Errorf("failed to get KV pairs for subtree: %v", err) } - + filteredPairs := FilterPairsByRange(pairs, startKey, endKey) return s.BuildMerkleTreeFromPairs(filteredPairs) -} \ No newline at end of file +} diff --git a/cluster/sync.go b/cluster/sync.go index 84cfda8..a6f5fbd 100644 --- a/cluster/sync.go +++ b/cluster/sync.go @@ -51,11 +51,11 @@ func (s *SyncService) Start() { s.logger.Info("Clustering disabled, skipping sync routines") return } - + // Start sync routine s.wg.Add(1) go s.syncRoutine() - + // Start Merkle tree rebuild routine s.wg.Add(1) go s.merkleTreeRebuildRoutine() @@ -172,9 +172,9 @@ func (s *SyncService) performMerkleSync() { // 2. Compare roots and start recursive diffing if they differ if !bytes.Equal(localRoot.Hash, remoteRoot.Hash) { s.logger.WithFields(logrus.Fields{ - "peer": peer.Address, - "local_root": hex.EncodeToString(localRoot.Hash), - "remote_root": hex.EncodeToString(remoteRoot.Hash), + "peer": peer.Address, + "local_root": hex.EncodeToString(localRoot.Hash), + "remote_root": hex.EncodeToString(remoteRoot.Hash), }).Info("Merkle roots differ, starting recursive diff") s.diffMerkleTreesRecursive(peer.Address, localRoot, remoteRoot) } else { @@ -186,10 +186,17 @@ func (s *SyncService) performMerkleSync() { // requestMerkleRoot requests the Merkle root from a peer func (s *SyncService) requestMerkleRoot(peerAddress string) (*types.MerkleRootResponse, error) { - client := &http.Client{Timeout: 10 * time.Second} - url := fmt.Sprintf("http://%s/merkle_tree/root", peerAddress) + client := NewAuthenticatedHTTPClient(s.config, 10*time.Second) + protocol := GetProtocol(s.config) + url := fmt.Sprintf("%s://%s/merkle_tree/root", protocol, peerAddress) - resp, err := client.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + AddClusterAuthHeaders(req, s.config) + + resp, err := client.Do(req) if err != nil { return nil, err } @@ -216,7 +223,7 @@ func (s *SyncService) diffMerkleTreesRecursive(peerAddress string, localNode, re // Hashes differ, need to go deeper. // Request children from the remote peer for the current range. req := types.MerkleTreeDiffRequest{ - ParentNode: *remoteNode, // We are asking the remote peer about its children for this range + ParentNode: *remoteNode, // We are asking the remote peer about its children for this range LocalHash: localNode.Hash, // Our hash for this range } @@ -294,10 +301,17 @@ func (s *SyncService) handleLeafLevelDiff(peerAddress string, keys []string, loc // fetchSingleKVFromPeer fetches a single KV pair from a peer func (s *SyncService) fetchSingleKVFromPeer(peerAddress, path string) (*types.StoredValue, error) { - client := &http.Client{Timeout: 5 * time.Second} - url := fmt.Sprintf("http://%s/kv/%s", peerAddress, path) + client := NewAuthenticatedHTTPClient(s.config, 5*time.Second) + protocol := GetProtocol(s.config) + url := fmt.Sprintf("%s://%s/kv/%s", protocol, peerAddress, path) - resp, err := client.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + AddClusterAuthHeaders(req, s.config) + + resp, err := client.Do(req) if err != nil { return nil, err } @@ -398,14 +412,14 @@ func (s *SyncService) resolveConflict(key string, local, remote *types.StoredVal // Timestamps are equal - need sophisticated conflict resolution s.logger.WithField("key", key).Info("Timestamp collision detected, applying oldest-node rule") - + // Get cluster members to determine which node is older members := s.gossipService.GetMembers() - + // Find the local node and the remote node in membership var localMember, remoteMember *types.Member localNodeID := s.config.NodeID - + for _, member := range members { if member.ID == localNodeID { localMember = member @@ -414,16 +428,16 @@ func (s *SyncService) resolveConflict(key string, local, remote *types.StoredVal remoteMember = member } } - + // If we can't find membership info, fall back to UUID comparison for deterministic result if localMember == nil || remoteMember == nil { s.logger.WithFields(logrus.Fields{ - "key": key, - "peerAddress": peerAddress, - "localNodeID": localNodeID, - "localMember": localMember != nil, - "remoteMember": remoteMember != nil, - "totalMembers": len(members), + "key": key, + "peerAddress": peerAddress, + "localNodeID": localNodeID, + "localMember": localMember != nil, + "remoteMember": remoteMember != nil, + "totalMembers": len(members), }).Warn("Could not find membership info for conflict resolution, using UUID comparison") if remote.UUID < local.UUID { // Remote UUID lexically smaller (deterministic choice) @@ -436,41 +450,49 @@ func (s *SyncService) resolveConflict(key string, local, remote *types.StoredVal s.logger.WithField("key", key).Info("Conflict resolved: local data wins (UUID tie-breaker)") return nil } - + // Apply oldest-node rule: node with earliest joined_timestamp wins if remoteMember.JoinedTimestamp < localMember.JoinedTimestamp { // Remote node is older, its data wins err := s.storeReplicatedDataWithMetadata(key, remote) if err == nil { s.logger.WithFields(logrus.Fields{ - "key": key, - "local_joined": localMember.JoinedTimestamp, - "remote_joined": remoteMember.JoinedTimestamp, + "key": key, + "local_joined": localMember.JoinedTimestamp, + "remote_joined": remoteMember.JoinedTimestamp, }).Info("Conflict resolved: remote data wins (oldest-node rule)") } return err } - + // Local node is older or equal, keep local data s.logger.WithFields(logrus.Fields{ - "key": key, - "local_joined": localMember.JoinedTimestamp, - "remote_joined": remoteMember.JoinedTimestamp, + "key": key, + "local_joined": localMember.JoinedTimestamp, + "remote_joined": remoteMember.JoinedTimestamp, }).Info("Conflict resolved: local data wins (oldest-node rule)") return nil } // requestMerkleDiff requests children hashes or keys for a given node/range from a peer -func (s *SyncService) requestMerkleDiff(peerAddress string, req types.MerkleTreeDiffRequest) (*types.MerkleTreeDiffResponse, error) { - jsonData, err := json.Marshal(req) +func (s *SyncService) requestMerkleDiff(peerAddress string, reqData types.MerkleTreeDiffRequest) (*types.MerkleTreeDiffResponse, error) { + jsonData, err := json.Marshal(reqData) if err != nil { return nil, err } - client := &http.Client{Timeout: 10 * time.Second} - url := fmt.Sprintf("http://%s/merkle_tree/diff", peerAddress) + client := NewAuthenticatedHTTPClient(s.config, 10*time.Second) + protocol := GetProtocol(s.config) + url := fmt.Sprintf("%s://%s/merkle_tree/diff", protocol, peerAddress) - resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + AddClusterAuthHeaders(req, s.config) + + resp, err := client.Do(req) if err != nil { return nil, err } @@ -525,20 +547,28 @@ func (s *SyncService) handleChildrenDiff(peerAddress string, children []types.Me // fetchAndStoreRange fetches a range of KV pairs from a peer and stores them locally func (s *SyncService) fetchAndStoreRange(peerAddress string, startKey, endKey string) error { - req := types.KVRangeRequest{ + reqData := types.KVRangeRequest{ StartKey: startKey, EndKey: endKey, Limit: 0, // No limit } - jsonData, err := json.Marshal(req) + jsonData, err := json.Marshal(reqData) if err != nil { return err } - client := &http.Client{Timeout: 30 * time.Second} // Longer timeout for range fetches - url := fmt.Sprintf("http://%s/kv_range", peerAddress) + client := NewAuthenticatedHTTPClient(s.config, 30*time.Second) // Longer timeout for range fetches + protocol := GetProtocol(s.config) + url := fmt.Sprintf("%s://%s/kv_range", protocol, peerAddress) - resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + AddClusterAuthHeaders(req, s.config) + + resp, err := client.Do(req) if err != nil { return err } @@ -568,4 +598,4 @@ func (s *SyncService) fetchAndStoreRange(peerAddress string, startKey, endKey st } } return nil -} \ No newline at end of file +} diff --git a/config/config.go b/config/config.go index 693331f..43089ab 100644 --- a/config/config.go +++ b/config/config.go @@ -1,12 +1,14 @@ package config import ( + "crypto/rand" + "encoding/base64" "fmt" "os" "path/filepath" - "kvs/types" "gopkg.in/yaml.v3" + "kvs/types" ) // Default configuration @@ -27,41 +29,61 @@ func Default() *types.Config { BootstrapMaxAgeHours: 720, // 30 days ThrottleDelayMs: 100, FetchDelayMs: 50, - + // Default compression settings CompressionEnabled: true, CompressionLevel: 3, // Balance between performance and compression ratio - + // Default TTL and size limit settings - DefaultTTL: "0", // No default TTL - MaxJSONSize: 1048576, // 1MB default max JSON size - + DefaultTTL: "0", // No default TTL + MaxJSONSize: 1048576, // 1MB default max JSON size + // Default rate limiting settings RateLimitRequests: 100, // 100 requests per window RateLimitWindow: "1m", // 1 minute window - + // Default tamper-evident logging settings TamperLogActions: []string{"data_write", "user_create", "auth_failure"}, - + // Default backup system settings BackupEnabled: true, BackupSchedule: "0 0 * * *", // Daily at midnight BackupPath: "./backups", BackupRetention: 7, // Keep backups for 7 days - + // Default feature toggle settings (all enabled by default) AuthEnabled: true, TamperLoggingEnabled: true, ClusteringEnabled: true, RateLimitingEnabled: true, RevisionHistoryEnabled: true, - + // Default anonymous access settings (both disabled by default for security) - AllowAnonymousRead: false, - AllowAnonymousWrite: false, + AllowAnonymousRead: false, + AllowAnonymousWrite: false, + + // Default cluster authentication settings (Issue #13) + ClusterSecret: generateClusterSecret(), + ClusterTLSEnabled: false, + ClusterTLSCertFile: "", + ClusterTLSKeyFile: "", + ClusterTLSSkipVerify: false, } } +// generateClusterSecret generates a cryptographically secure random cluster secret +func generateClusterSecret() string { + // Generate 32 bytes (256 bits) of random data + randomBytes := make([]byte, 32) + if _, err := rand.Read(randomBytes); err != nil { + // Fallback to a warning - this should never happen in practice + fmt.Fprintf(os.Stderr, "Warning: Failed to generate secure cluster secret: %v\n", err) + return "" + } + // Encode as base64 for easy configuration file storage + return base64.StdEncoding.EncodeToString(randomBytes) +} + // Load configuration from file or create default func Load(configPath string) (*types.Config, error) { config := Default() @@ -94,5 +116,13 @@ func Load(configPath string) (*types.Config, error) { return nil, fmt.Errorf("failed to parse config file: %v", err) } + // Generate cluster secret if not provided and clustering is enabled (Issue #13) + if config.ClusteringEnabled && config.ClusterSecret == "" { + config.ClusterSecret = generateClusterSecret() + fmt.Printf("Warning: No cluster_secret configured. Generated a random secret.\n") + fmt.Printf(" To share this secret with other nodes, add it to your config:\n") + fmt.Printf(" cluster_secret: %s\n", config.ClusterSecret) + } + return config, nil -} \ No newline at end of file +} diff --git a/features/auth.go b/features/auth.go index 65580a5..8b9ffd6 100644 --- a/features/auth.go +++ b/features/auth.go @@ -99,4 +99,4 @@ func ExtractKVResourceKey(r *http.Request) string { return path } return "" -} \ No newline at end of file +} diff --git a/features/backup.go b/features/backup.go index 893351b..f73e4da 100644 --- a/features/backup.go +++ b/features/backup.go @@ -8,4 +8,4 @@ import ( // GetBackupFilename generates a filename for a backup func GetBackupFilename(timestamp time.Time) string { return fmt.Sprintf("kvs-backup-%s.zstd", timestamp.Format("2006-01-02")) -} \ No newline at end of file +} diff --git a/features/features.go b/features/features.go index 6ee027e..e86694c 100644 --- a/features/features.go +++ b/features/features.go @@ -1,4 +1,4 @@ // Package features provides utility functions for KVS authentication, validation, // logging, backup, and other operational features. These functions were extracted // from main.go to improve code organization and maintainability. -package features \ No newline at end of file +package features diff --git a/features/ratelimit.go b/features/ratelimit.go index 99ef49b..5906e0e 100644 --- a/features/ratelimit.go +++ b/features/ratelimit.go @@ -5,4 +5,4 @@ import "fmt" // GetRateLimitKey generates the storage key for rate limiting func GetRateLimitKey(userUUID string, windowStart int64) string { return fmt.Sprintf("ratelimit:%s:%d", userUUID, windowStart) -} \ No newline at end of file +} diff --git a/features/revision.go b/features/revision.go index d348010..395df73 100644 --- a/features/revision.go +++ b/features/revision.go @@ -5,4 +5,4 @@ import "fmt" // GetRevisionKey generates the storage key for a specific revision func GetRevisionKey(baseKey string, revision int) string { return fmt.Sprintf("%s:rev:%d", baseKey, revision) -} \ No newline at end of file +} diff --git a/features/tamperlog.go b/features/tamperlog.go index 6e57f21..8e5c569 100644 --- a/features/tamperlog.go +++ b/features/tamperlog.go @@ -21,4 +21,4 @@ func GenerateLogSignature(timestamp, action, userUUID, resource string) string { // Concatenate all fields in a deterministic order data := fmt.Sprintf("%s|%s|%s|%s", timestamp, action, userUUID, resource) return utils.HashSHA3512(data) -} \ No newline at end of file +} diff --git a/features/validation.go b/features/validation.go index 096c3b5..de570df 100644 --- a/features/validation.go +++ b/features/validation.go @@ -21,4 +21,4 @@ func ParseTTL(ttlString string) (time.Duration, error) { } return duration, nil -} \ No newline at end of file +} diff --git a/main.go b/main.go index ebcc2d9..4d858ef 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,6 @@ import ( "kvs/server" ) - func main() { configPath := "./config.yaml" diff --git a/server/handlers.go b/server/handlers.go index 843f530..25184d9 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -22,8 +22,6 @@ import ( "kvs/utils" ) - - // healthHandler returns server health status func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { mode := s.getMode() @@ -1271,3 +1269,29 @@ func (s *Server) getRevisionHistory(key string) ([]map[string]interface{}, error func (s *Server) getSpecificRevision(key string, revision int) (*types.StoredValue, error) { return s.revisionService.GetSpecificRevision(key, revision) } + +// clusterBootstrapHandler provides the cluster secret to authenticated administrators (Issue #13) +func (s *Server) clusterBootstrapHandler(w http.ResponseWriter, r *http.Request) { + // Ensure clustering is enabled + if !s.config.ClusteringEnabled { + http.Error(w, "Clustering is disabled", http.StatusServiceUnavailable) + return + } + + // Ensure cluster secret is configured + if s.config.ClusterSecret == "" { + s.logger.Error("Cluster secret is not configured") + http.Error(w, "Cluster secret is not configured", http.StatusInternalServerError) + return + } + + // Return the cluster secret for secure bootstrap + response := map[string]string{ + "cluster_secret": s.config.ClusterSecret, + } + + s.logger.WithField("remote_addr", r.RemoteAddr).Info("Cluster secret retrieved for bootstrap") + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} diff --git a/server/routes.go b/server/routes.go index 814e5f7..297e647 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1,6 +1,8 @@ package server import ( + "net/http" + "github.com/gorilla/mux" ) @@ -20,7 +22,7 @@ func (s *Server) setupRoutes() *mux.Router { } else { router.HandleFunc("/kv/{path:.+}", s.getKVHandler).Methods("GET") } - + // PUT endpoint - require auth if anonymous write is disabled if s.config.AuthEnabled && !s.config.AllowAnonymousWrite { router.Handle("/kv/{path:.+}", s.authService.Middleware( @@ -29,7 +31,7 @@ func (s *Server) setupRoutes() *mux.Router { } else { router.HandleFunc("/kv/{path:.+}", s.putKVHandler).Methods("PUT") } - + // DELETE endpoint - always require authentication (no anonymous delete) if s.config.AuthEnabled { router.Handle("/kv/{path:.+}", s.authService.Middleware( @@ -41,16 +43,31 @@ func (s *Server) setupRoutes() *mux.Router { // Member endpoints (available when clustering is enabled) if s.config.ClusteringEnabled { - router.HandleFunc("/members/", s.getMembersHandler).Methods("GET") - router.HandleFunc("/members/join", s.joinMemberHandler).Methods("POST") - router.HandleFunc("/members/leave", s.leaveMemberHandler).Methods("DELETE") - router.HandleFunc("/members/gossip", s.gossipHandler).Methods("POST") - router.HandleFunc("/members/pairs_by_time", s.pairsByTimeHandler).Methods("POST") + // Apply cluster authentication middleware if cluster secret is configured + if s.clusterAuthService != nil { + router.Handle("/members/", s.clusterAuthService.Middleware(http.HandlerFunc(s.getMembersHandler))).Methods("GET") + router.Handle("/members/join", s.clusterAuthService.Middleware(http.HandlerFunc(s.joinMemberHandler))).Methods("POST") + router.Handle("/members/leave", s.clusterAuthService.Middleware(http.HandlerFunc(s.leaveMemberHandler))).Methods("DELETE") + router.Handle("/members/gossip", s.clusterAuthService.Middleware(http.HandlerFunc(s.gossipHandler))).Methods("POST") + router.Handle("/members/pairs_by_time", s.clusterAuthService.Middleware(http.HandlerFunc(s.pairsByTimeHandler))).Methods("POST") - // Merkle Tree endpoints (clustering feature) - router.HandleFunc("/merkle_tree/root", s.getMerkleRootHandler).Methods("GET") - router.HandleFunc("/merkle_tree/diff", s.getMerkleDiffHandler).Methods("POST") - router.HandleFunc("/kv_range", s.getKVRangeHandler).Methods("POST") + // Merkle Tree endpoints (clustering feature) + router.Handle("/merkle_tree/root", s.clusterAuthService.Middleware(http.HandlerFunc(s.getMerkleRootHandler))).Methods("GET") + router.Handle("/merkle_tree/diff", s.clusterAuthService.Middleware(http.HandlerFunc(s.getMerkleDiffHandler))).Methods("POST") + router.Handle("/kv_range", s.clusterAuthService.Middleware(http.HandlerFunc(s.getKVRangeHandler))).Methods("POST") + } else { + // Fallback to unprotected endpoints (for backwards compatibility) + router.HandleFunc("/members/", s.getMembersHandler).Methods("GET") + router.HandleFunc("/members/join", s.joinMemberHandler).Methods("POST") + router.HandleFunc("/members/leave", s.leaveMemberHandler).Methods("DELETE") + router.HandleFunc("/members/gossip", s.gossipHandler).Methods("POST") + router.HandleFunc("/members/pairs_by_time", s.pairsByTimeHandler).Methods("POST") + + // Merkle Tree endpoints (clustering feature) + router.HandleFunc("/merkle_tree/root", s.getMerkleRootHandler).Methods("GET") + router.HandleFunc("/merkle_tree/diff", s.getMerkleDiffHandler).Methods("POST") + router.HandleFunc("/kv_range", s.getKVRangeHandler).Methods("POST") + } } // Authentication and user management endpoints (available when auth is enabled) @@ -59,15 +76,15 @@ func (s *Server) setupRoutes() *mux.Router { router.Handle("/api/users", s.authService.Middleware( []string{"admin:users:create"}, nil, "", )(s.createUserHandler)).Methods("POST") - + router.Handle("/api/users/{uuid}", s.authService.Middleware( []string{"admin:users:read"}, nil, "", )(s.getUserHandler)).Methods("GET") - + router.Handle("/api/users/{uuid}", s.authService.Middleware( []string{"admin:users:update"}, nil, "", )(s.updateUserHandler)).Methods("PUT") - + router.Handle("/api/users/{uuid}", s.authService.Middleware( []string{"admin:users:delete"}, nil, "", )(s.deleteUserHandler)).Methods("DELETE") @@ -76,15 +93,15 @@ func (s *Server) setupRoutes() *mux.Router { router.Handle("/api/groups", s.authService.Middleware( []string{"admin:groups:create"}, nil, "", )(s.createGroupHandler)).Methods("POST") - + router.Handle("/api/groups/{uuid}", s.authService.Middleware( []string{"admin:groups:read"}, nil, "", )(s.getGroupHandler)).Methods("GET") - + router.Handle("/api/groups/{uuid}", s.authService.Middleware( []string{"admin:groups:update"}, nil, "", )(s.updateGroupHandler)).Methods("PUT") - + router.Handle("/api/groups/{uuid}", s.authService.Middleware( []string{"admin:groups:delete"}, nil, "", )(s.deleteGroupHandler)).Methods("DELETE") @@ -93,6 +110,12 @@ func (s *Server) setupRoutes() *mux.Router { router.Handle("/api/tokens", s.authService.Middleware( []string{"admin:tokens:create"}, nil, "", )(s.createTokenHandler)).Methods("POST") + + // Cluster Bootstrap endpoint (Issue #13) - Protected by JWT authentication + // Allows authenticated administrators to retrieve the cluster secret for new nodes + router.Handle("/auth/cluster-bootstrap", s.authService.Middleware( + []string{"admin:tokens:create"}, nil, "", + )(s.clusterBootstrapHandler)).Methods("GET") } // Revision History endpoints (available when revision history is enabled) diff --git a/server/server.go b/server/server.go index 22735a9..41b6e37 100644 --- a/server/server.go +++ b/server/server.go @@ -50,7 +50,8 @@ type Server struct { backupMu sync.RWMutex // Protects backup status // Authentication service - authService *auth.AuthService + authService *auth.AuthService + clusterAuthService *auth.ClusterAuthService } // NewServer initializes and returns a new Server instance @@ -120,6 +121,11 @@ func NewServer(config *types.Config) (*Server, error) { // Initialize authentication service server.authService = auth.NewAuthService(db, logger, config) + // Initialize cluster authentication service (Issue #13) + if config.ClusteringEnabled { + server.clusterAuthService = auth.NewClusterAuthService(config.ClusterSecret, logger) + } + // Setup initial root account if needed (Issue #3) if config.AuthEnabled { if err := server.setupRootAccount(); err != nil { @@ -219,7 +225,7 @@ func (s *Server) setupRootAccount() error { func (s *Server) createRootUserAndToken() error { rootNickname := "root" adminGroupName := "admin" - + // Generate UUIDs rootUserUUID := "root-" + time.Now().Format("20060102-150405") adminGroupUUID := "admin-" + time.Now().Format("20060102-150405") @@ -234,7 +240,7 @@ func (s *Server) createRootUserAndToken() error { UpdatedAt: now, } - // Create root user + // Create root user rootUser := types.User{ UUID: rootUserUUID, NicknameHash: hashUserNickname(rootNickname), @@ -251,7 +257,7 @@ func (s *Server) createRootUserAndToken() error { // Create API token with full administrative scopes adminScopes := []string{ "admin:users:create", "admin:users:read", "admin:users:update", "admin:users:delete", - "admin:groups:create", "admin:groups:read", "admin:groups:update", "admin:groups:delete", + "admin:groups:create", "admin:groups:read", "admin:groups:update", "admin:groups:delete", "admin:tokens:create", "admin:tokens:revoke", "read", "write", "delete", } @@ -269,13 +275,13 @@ func (s *Server) createRootUserAndToken() error { // Log the token securely (one-time display) s.logger.WithFields(logrus.Fields{ - "user_uuid": rootUserUUID, - "group_uuid": adminGroupUUID, - "expires_at": time.Unix(expiresAt, 0).Format(time.RFC3339), - "expires_in": "24 hours", + "user_uuid": rootUserUUID, + "group_uuid": adminGroupUUID, + "expires_at": time.Unix(expiresAt, 0).Format(time.RFC3339), + "expires_in": "24 hours", }).Warn("Root account created - SAVE THIS TOKEN:") - // Display token prominently + // Display token prominently fmt.Printf("\n" + strings.Repeat("=", 80) + "\n") fmt.Printf("🔐 ROOT ACCOUNT CREATED - INITIAL SETUP TOKEN\n") fmt.Printf("===========================================\n") @@ -309,7 +315,7 @@ func (s *Server) storeUserAndGroup(user *types.User, group *types.Group) error { if err != nil { return fmt.Errorf("failed to marshal user data: %v", err) } - + if err := txn.Set([]byte(auth.UserStorageKey(user.UUID)), userData); err != nil { return fmt.Errorf("failed to store user: %v", err) } @@ -319,7 +325,7 @@ func (s *Server) storeUserAndGroup(user *types.User, group *types.Group) error { if err != nil { return fmt.Errorf("failed to marshal group data: %v", err) } - + if err := txn.Set([]byte(auth.GroupStorageKey(group.UUID)), groupData); err != nil { return fmt.Errorf("failed to store group: %v", err) } @@ -327,4 +333,3 @@ func (s *Server) storeUserAndGroup(user *types.User, group *types.Group) error { return nil }) } - diff --git a/storage/compression.go b/storage/compression.go index 0a635de..487599c 100644 --- a/storage/compression.go +++ b/storage/compression.go @@ -2,7 +2,7 @@ package storage import ( "fmt" - + "github.com/klauspost/compress/zstd" ) @@ -57,4 +57,4 @@ func (c *CompressionService) DecompressData(compressedData []byte) ([]byte, erro return nil, fmt.Errorf("decompressor not initialized") } return c.decompressor.DecodeAll(compressedData, nil) -} \ No newline at end of file +} diff --git a/storage/revision.go b/storage/revision.go index 25f35c2..c248701 100644 --- a/storage/revision.go +++ b/storage/revision.go @@ -34,10 +34,10 @@ func GetRevisionKey(baseKey string, revision int) string { 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 { @@ -60,7 +60,7 @@ func (r *RevisionService) StoreRevisionHistory(txn *badger.Txn, key string, stor 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{} @@ -69,13 +69,13 @@ func (r *RevisionService) StoreRevisionHistory(txn *badger.Txn, key string, stor 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)) @@ -83,8 +83,8 @@ func (r *RevisionService) StoreRevisionHistory(txn *badger.Txn, key string, stor rev3Key := GetRevisionKey(key, 3) r.storage.StoreWithTTL(txn, []byte(rev3Key), rev2Data, ttl) } - - // Shift rev:1 → rev:2 + + // Shift rev:1 → rev:2 rev1Key := GetRevisionKey(key, 1) rev1Data, err := r.storage.RetrieveWithDecompression(txn, []byte(rev1Key)) if err == nil { @@ -92,80 +92,80 @@ func (r *RevisionService) StoreRevisionHistory(txn *badger.Txn, key string, stor 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 } @@ -174,23 +174,23 @@ func (r *RevisionService) GetSpecificRevision(key string, revision int) (*types. 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 } @@ -200,15 +200,15 @@ func GetRevisionFromPath(path string) (string, int, error) { 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 -} \ No newline at end of file +} diff --git a/storage/storage.go b/storage/storage.go index 952725d..d16ec03 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -12,17 +12,17 @@ import ( // StorageService handles all BadgerDB operations and data management type StorageService struct { - db *badger.DB - config *types.Config - compressionSvc *CompressionService - logger *logrus.Logger + db *badger.DB + config *types.Config + compressionSvc *CompressionService + logger *logrus.Logger } // NewStorageService creates a new storage service func NewStorageService(db *badger.DB, config *types.Config, logger *logrus.Logger) (*StorageService, error) { var compressionSvc *CompressionService var err error - + // Initialize compression if enabled if config.CompressionEnabled { compressionSvc, err = NewCompressionService() @@ -50,7 +50,7 @@ func (s *StorageService) Close() { func (s *StorageService) StoreWithTTL(txn *badger.Txn, key []byte, data []byte, ttl time.Duration) error { var finalData []byte var err error - + // Compress data if compression is enabled if s.config.CompressionEnabled && s.compressionSvc != nil { finalData, err = s.compressionSvc.CompressData(data) @@ -60,14 +60,14 @@ func (s *StorageService) StoreWithTTL(txn *badger.Txn, key []byte, data []byte, } else { finalData = data } - + entry := badger.NewEntry(key, finalData) - + // Apply TTL if specified if ttl > 0 { entry = entry.WithTTL(ttl) } - + return txn.SetEntry(entry) } @@ -77,7 +77,7 @@ func (s *StorageService) RetrieveWithDecompression(txn *badger.Txn, key []byte) if err != nil { return nil, err } - + var compressedData []byte err = item.Value(func(val []byte) error { compressedData = append(compressedData, val...) @@ -86,12 +86,12 @@ func (s *StorageService) RetrieveWithDecompression(txn *badger.Txn, key []byte) if err != nil { return nil, err } - + // Decompress data if compression is enabled if s.config.CompressionEnabled && s.compressionSvc != nil { return s.compressionSvc.DecompressData(compressedData) } - + return compressedData, nil } @@ -109,4 +109,4 @@ func (s *StorageService) DecompressData(compressedData []byte) ([]byte, error) { return compressedData, nil } return s.compressionSvc.DecompressData(compressedData) -} \ No newline at end of file +} diff --git a/types/types.go b/types/types.go index bcdf027..1032669 100644 --- a/types/types.go +++ b/types/types.go @@ -13,20 +13,20 @@ type StoredValue struct { // User represents a system user type User struct { - UUID string `json:"uuid"` // Server-generated UUID + UUID string `json:"uuid"` // Server-generated UUID NicknameHash string `json:"nickname_hash"` // SHA3-512 hash of nickname - Groups []string `json:"groups"` // List of group UUIDs this user belongs to - CreatedAt int64 `json:"created_at"` // Unix timestamp - UpdatedAt int64 `json:"updated_at"` // Unix timestamp + Groups []string `json:"groups"` // List of group UUIDs this user belongs to + CreatedAt int64 `json:"created_at"` // Unix timestamp + UpdatedAt int64 `json:"updated_at"` // Unix timestamp } // Group represents a user group type Group struct { - UUID string `json:"uuid"` // Server-generated UUID - NameHash string `json:"name_hash"` // SHA3-512 hash of group name - Members []string `json:"members"` // List of user UUIDs in this group - CreatedAt int64 `json:"created_at"` // Unix timestamp - UpdatedAt int64 `json:"updated_at"` // Unix timestamp + UUID string `json:"uuid"` // Server-generated UUID + NameHash string `json:"name_hash"` // SHA3-512 hash of group name + Members []string `json:"members"` // List of user UUIDs in this group + CreatedAt int64 `json:"created_at"` // Unix timestamp + UpdatedAt int64 `json:"updated_at"` // Unix timestamp } // APIToken represents a JWT authentication token @@ -40,12 +40,12 @@ type APIToken struct { // ResourceMetadata contains ownership and permission information for stored resources type ResourceMetadata struct { - OwnerUUID string `json:"owner_uuid"` // UUID of the resource owner - GroupUUID string `json:"group_uuid"` // UUID of the resource group - Permissions int `json:"permissions"` // 12-bit permission mask (POSIX-inspired) - TTL string `json:"ttl"` // Time-to-live duration (Go format) - CreatedAt int64 `json:"created_at"` // Unix timestamp when resource was created - UpdatedAt int64 `json:"updated_at"` // Unix timestamp when resource was last updated + OwnerUUID string `json:"owner_uuid"` // UUID of the resource owner + GroupUUID string `json:"group_uuid"` // UUID of the resource group + Permissions int `json:"permissions"` // 12-bit permission mask (POSIX-inspired) + TTL string `json:"ttl"` // Time-to-live duration (Go format) + CreatedAt int64 `json:"created_at"` // Unix timestamp when resource was created + UpdatedAt int64 `json:"updated_at"` // Unix timestamp when resource was last updated } // Permission constants for POSIX-inspired ACL @@ -55,19 +55,19 @@ const ( PermOwnerDelete = 1 << 10 PermOwnerWrite = 1 << 9 PermOwnerRead = 1 << 8 - + // Group permissions (bits 7-4) PermGroupCreate = 1 << 7 PermGroupDelete = 1 << 6 PermGroupWrite = 1 << 5 PermGroupRead = 1 << 4 - + // Others permissions (bits 3-0) PermOthersCreate = 1 << 3 PermOthersDelete = 1 << 2 PermOthersWrite = 1 << 1 PermOthersRead = 1 << 0 - + // Default permissions: Owner(1111), Group(0110), Others(0010) DefaultPermissions = (PermOwnerCreate | PermOwnerDelete | PermOwnerWrite | PermOwnerRead) | (PermGroupWrite | PermGroupRead) | @@ -231,50 +231,57 @@ type KVRangeResponse struct { // Configuration type Config struct { - NodeID string `yaml:"node_id"` - BindAddress string `yaml:"bind_address"` - Port int `yaml:"port"` - DataDir string `yaml:"data_dir"` - SeedNodes []string `yaml:"seed_nodes"` - ReadOnly bool `yaml:"read_only"` - LogLevel string `yaml:"log_level"` - GossipIntervalMin int `yaml:"gossip_interval_min"` - GossipIntervalMax int `yaml:"gossip_interval_max"` - SyncInterval int `yaml:"sync_interval"` - CatchupInterval int `yaml:"catchup_interval"` - BootstrapMaxAgeHours int `yaml:"bootstrap_max_age_hours"` - ThrottleDelayMs int `yaml:"throttle_delay_ms"` - FetchDelayMs int `yaml:"fetch_delay_ms"` - + NodeID string `yaml:"node_id"` + BindAddress string `yaml:"bind_address"` + Port int `yaml:"port"` + DataDir string `yaml:"data_dir"` + SeedNodes []string `yaml:"seed_nodes"` + ReadOnly bool `yaml:"read_only"` + LogLevel string `yaml:"log_level"` + GossipIntervalMin int `yaml:"gossip_interval_min"` + GossipIntervalMax int `yaml:"gossip_interval_max"` + SyncInterval int `yaml:"sync_interval"` + CatchupInterval int `yaml:"catchup_interval"` + BootstrapMaxAgeHours int `yaml:"bootstrap_max_age_hours"` + ThrottleDelayMs int `yaml:"throttle_delay_ms"` + FetchDelayMs int `yaml:"fetch_delay_ms"` + // Database compression configuration CompressionEnabled bool `yaml:"compression_enabled"` CompressionLevel int `yaml:"compression_level"` - + // TTL configuration - DefaultTTL string `yaml:"default_ttl"` // Go duration format, "0" means no default TTL - MaxJSONSize int `yaml:"max_json_size"` // Maximum JSON size in bytes - + DefaultTTL string `yaml:"default_ttl"` // Go duration format, "0" means no default TTL + MaxJSONSize int `yaml:"max_json_size"` // Maximum JSON size in bytes + // Rate limiting configuration RateLimitRequests int `yaml:"rate_limit_requests"` // Max requests per window RateLimitWindow string `yaml:"rate_limit_window"` // Window duration (Go format) - + // Tamper-evident logging configuration TamperLogActions []string `yaml:"tamper_log_actions"` // Actions to log - + // Backup system configuration - BackupEnabled bool `yaml:"backup_enabled"` // Enable/disable automated backups - BackupSchedule string `yaml:"backup_schedule"` // Cron schedule format - BackupPath string `yaml:"backup_path"` // Directory to store backups - BackupRetention int `yaml:"backup_retention"` // Days to keep backups - + BackupEnabled bool `yaml:"backup_enabled"` // Enable/disable automated backups + BackupSchedule string `yaml:"backup_schedule"` // Cron schedule format + BackupPath string `yaml:"backup_path"` // Directory to store backups + BackupRetention int `yaml:"backup_retention"` // Days to keep backups + // Feature toggles for optional functionalities AuthEnabled bool `yaml:"auth_enabled"` // Enable/disable authentication system TamperLoggingEnabled bool `yaml:"tamper_logging_enabled"` // Enable/disable tamper-evident logging ClusteringEnabled bool `yaml:"clustering_enabled"` // Enable/disable clustering/gossip RateLimitingEnabled bool `yaml:"rate_limiting_enabled"` // Enable/disable rate limiting RevisionHistoryEnabled bool `yaml:"revision_history_enabled"` // Enable/disable revision history - + // Anonymous access control (Issue #5) - AllowAnonymousRead bool `yaml:"allow_anonymous_read"` // Allow unauthenticated read access to KV endpoints - AllowAnonymousWrite bool `yaml:"allow_anonymous_write"` // Allow unauthenticated write access to KV endpoints -} \ No newline at end of file + AllowAnonymousRead bool `yaml:"allow_anonymous_read"` // Allow unauthenticated read access to KV endpoints + AllowAnonymousWrite bool `yaml:"allow_anonymous_write"` // Allow unauthenticated write access to KV endpoints + + // Cluster authentication (Issue #13) + ClusterSecret string `yaml:"cluster_secret"` // Shared secret for cluster authentication (auto-generated if empty) + ClusterTLSEnabled bool `yaml:"cluster_tls_enabled"` // Require TLS for inter-node communication + ClusterTLSCertFile string `yaml:"cluster_tls_cert_file"` // Path to TLS certificate file + ClusterTLSKeyFile string `yaml:"cluster_tls_key_file"` // Path to TLS private key file + ClusterTLSSkipVerify bool `yaml:"cluster_tls_skip_verify"` // Skip TLS verification (insecure, for testing only) +} diff --git a/utils/hash.go b/utils/hash.go index 1595b6a..ed23efa 100644 --- a/utils/hash.go +++ b/utils/hash.go @@ -22,4 +22,4 @@ func HashGroupName(groupname string) string { func HashToken(token string) string { return HashSHA3512(token) -} \ No newline at end of file +}