Compare commits
	
		
			1 Commits
		
	
	
		
			metadata-a
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					32b347f1fd | 
							
								
								
									
										108
									
								
								auth/auth.go
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								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 {
 | 
			
		||||
@@ -198,12 +198,61 @@ func (s *AuthService) CheckResourcePermission(authCtx *AuthContext, resourceKey
 | 
			
		||||
	return CheckPermission(metadata.Permissions, operation, isOwner, isGroupMember)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetResourceMetadata retrieves metadata for a resource
 | 
			
		||||
func (s *AuthService) GetResourceMetadata(resourceKey string) (*types.ResourceMetadata, error) {
 | 
			
		||||
	var metadata types.ResourceMetadata
 | 
			
		||||
// GetAuthContext retrieves auth context from request context
 | 
			
		||||
func GetAuthContext(ctx context.Context) *AuthContext {
 | 
			
		||||
	if authCtx, ok := ctx.Value("auth").(*AuthContext); ok {
 | 
			
		||||
		return authCtx
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
		item, err := txn.Get([]byte(ResourceMetadataKey(resourceKey)))
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StoreResourceMetadata stores or updates resource metadata in BadgerDB
 | 
			
		||||
func (s *AuthService) StoreResourceMetadata(path string, metadata *types.ResourceMetadata) error {
 | 
			
		||||
	now := time.Now().Unix()
 | 
			
		||||
	if metadata.CreatedAt == 0 {
 | 
			
		||||
		metadata.CreatedAt = now
 | 
			
		||||
	}
 | 
			
		||||
	metadata.UpdatedAt = now
 | 
			
		||||
 | 
			
		||||
	metadataData, err := json.Marshal(metadata)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s.db.Update(func(txn *badger.Txn) error {
 | 
			
		||||
		return txn.Set([]byte(ResourceMetadataKey(path)), metadataData)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetResourceMetadata retrieves resource metadata from BadgerDB
 | 
			
		||||
func (s *AuthService) GetResourceMetadata(path string) (*types.ResourceMetadata, error) {
 | 
			
		||||
	var metadata types.ResourceMetadata
 | 
			
		||||
	
 | 
			
		||||
	err := s.db.View(func(txn *badger.Txn) error {
 | 
			
		||||
		item, err := txn.Get([]byte(ResourceMetadataKey(path)))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -219,46 +268,3 @@ func (s *AuthService) GetResourceMetadata(resourceKey string) (*types.ResourceMe
 | 
			
		||||
 | 
			
		||||
	return &metadata, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetResourceMetadata stores metadata for a resource
 | 
			
		||||
func (s *AuthService) SetResourceMetadata(resourceKey string, metadata *types.ResourceMetadata) error {
 | 
			
		||||
	metadataBytes, err := json.Marshal(metadata)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to marshal metadata: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s.db.Update(func(txn *badger.Txn) error {
 | 
			
		||||
		return txn.Set([]byte(ResourceMetadataKey(resourceKey)), metadataBytes)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAuthContext retrieves auth context from request context
 | 
			
		||||
func GetAuthContext(ctx context.Context) *AuthContext {
 | 
			
		||||
	if authCtx, ok := ctx.Value("auth").(*AuthContext); ok {
 | 
			
		||||
		return authCtx
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -64,4 +64,4 @@ func ValidateJWT(tokenString string) (*JWTClaims, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, fmt.Errorf("invalid token")
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -16,4 +16,4 @@ func TokenStorageKey(tokenHash string) string {
 | 
			
		||||
 | 
			
		||||
func ResourceMetadataKey(resourceKey string) string {
 | 
			
		||||
	return resourceKey + ":metadata"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -82,19 +82,10 @@ func (s *BootstrapService) attemptJoin(seedAddr string) bool {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := NewAuthenticatedHTTPClient(s.config, 10*time.Second)
 | 
			
		||||
	protocol := GetProtocol(s.config)
 | 
			
		||||
	url := fmt.Sprintf("%s://%s/members/join", protocol, seedAddr)
 | 
			
		||||
	client := &http.Client{Timeout: 10 * time.Second}
 | 
			
		||||
	url := fmt.Sprintf("http://%s/members/join", seedAddr)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.WithError(err).Error("Failed to create join request")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	AddClusterAuthHeaders(req, s.config)
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.WithFields(logrus.Fields{
 | 
			
		||||
			"seed":  seedAddr,
 | 
			
		||||
@@ -151,4 +142,4 @@ func (s *BootstrapService) performGradualSync() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.logger.Info("Gradual sync completed")
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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,20 +181,11 @@ func (s *GossipService) gossipWithPeer(peer *types.Member) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
	// Send HTTP request to peer
 | 
			
		||||
	client := &http.Client{Timeout: 5 * time.Second}
 | 
			
		||||
	url := fmt.Sprintf("http://%s/members/gossip", peer.Address)
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.WithFields(logrus.Fields{
 | 
			
		||||
			"peer":  peer.Address,
 | 
			
		||||
@@ -309,4 +300,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()
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
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"
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								cluster/sync.go
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								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,17 +186,10 @@ func (s *SyncService) performMerkleSync() {
 | 
			
		||||
 | 
			
		||||
// requestMerkleRoot requests the Merkle root from a peer
 | 
			
		||||
func (s *SyncService) requestMerkleRoot(peerAddress string) (*types.MerkleRootResponse, error) {
 | 
			
		||||
	client := NewAuthenticatedHTTPClient(s.config, 10*time.Second)
 | 
			
		||||
	protocol := GetProtocol(s.config)
 | 
			
		||||
	url := fmt.Sprintf("%s://%s/merkle_tree/root", protocol, peerAddress)
 | 
			
		||||
	client := &http.Client{Timeout: 10 * time.Second}
 | 
			
		||||
	url := fmt.Sprintf("http://%s/merkle_tree/root", peerAddress)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	AddClusterAuthHeaders(req, s.config)
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	resp, err := client.Get(url)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -223,7 +216,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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -301,17 +294,10 @@ 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 := NewAuthenticatedHTTPClient(s.config, 5*time.Second)
 | 
			
		||||
	protocol := GetProtocol(s.config)
 | 
			
		||||
	url := fmt.Sprintf("%s://%s/kv/%s", protocol, peerAddress, path)
 | 
			
		||||
	client := &http.Client{Timeout: 5 * time.Second}
 | 
			
		||||
	url := fmt.Sprintf("http://%s/kv/%s", peerAddress, path)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	AddClusterAuthHeaders(req, s.config)
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	resp, err := client.Get(url)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -412,14 +398,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
 | 
			
		||||
@@ -428,16 +414,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)
 | 
			
		||||
@@ -450,49 +436,41 @@ 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, reqData types.MerkleTreeDiffRequest) (*types.MerkleTreeDiffResponse, error) {
 | 
			
		||||
	jsonData, err := json.Marshal(reqData)
 | 
			
		||||
func (s *SyncService) requestMerkleDiff(peerAddress string, req types.MerkleTreeDiffRequest) (*types.MerkleTreeDiffResponse, error) {
 | 
			
		||||
	jsonData, err := json.Marshal(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := NewAuthenticatedHTTPClient(s.config, 10*time.Second)
 | 
			
		||||
	protocol := GetProtocol(s.config)
 | 
			
		||||
	url := fmt.Sprintf("%s://%s/merkle_tree/diff", protocol, peerAddress)
 | 
			
		||||
	client := &http.Client{Timeout: 10 * time.Second}
 | 
			
		||||
	url := fmt.Sprintf("http://%s/merkle_tree/diff", peerAddress)
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -547,28 +525,20 @@ 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 {
 | 
			
		||||
	reqData := types.KVRangeRequest{
 | 
			
		||||
	req := types.KVRangeRequest{
 | 
			
		||||
		StartKey: startKey,
 | 
			
		||||
		EndKey:   endKey,
 | 
			
		||||
		Limit:    0, // No limit
 | 
			
		||||
	}
 | 
			
		||||
	jsonData, err := json.Marshal(reqData)
 | 
			
		||||
	jsonData, err := json.Marshal(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	client := &http.Client{Timeout: 30 * time.Second} // Longer timeout for range fetches
 | 
			
		||||
	url := fmt.Sprintf("http://%s/kv_range", peerAddress)
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -598,4 +568,4 @@ func (s *SyncService) fetchAndStoreRange(peerAddress string, startKey, endKey st
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +1,12 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
	"kvs/types"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Default configuration
 | 
			
		||||
@@ -29,61 +27,41 @@ 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,
 | 
			
		||||
 | 
			
		||||
		// Default cluster authentication settings (Issue #13)
 | 
			
		||||
		ClusterSecret:        generateClusterSecret(),
 | 
			
		||||
		ClusterTLSEnabled:    false,
 | 
			
		||||
		ClusterTLSCertFile:   "",
 | 
			
		||||
		ClusterTLSKeyFile:    "",
 | 
			
		||||
		ClusterTLSSkipVerify: false,
 | 
			
		||||
		AllowAnonymousRead:     false,
 | 
			
		||||
		AllowAnonymousWrite:    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()
 | 
			
		||||
@@ -116,13 +94,5 @@ 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
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -99,4 +99,4 @@ func ExtractKVResourceKey(r *http.Request) string {
 | 
			
		||||
		return path
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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"))
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
package features
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -21,4 +21,4 @@ func ParseTTL(ttlString string) (time.Duration, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return duration, nil
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -124,10 +124,7 @@ EOF
 | 
			
		||||
# Test 3: Cluster formation
 | 
			
		||||
test_cluster_formation() {
 | 
			
		||||
    test_start "2-node cluster formation and Merkle Tree replication"
 | 
			
		||||
 | 
			
		||||
    # Shared cluster secret for authentication (Issue #13)
 | 
			
		||||
    local CLUSTER_SECRET="test-cluster-secret-12345678901234567890"
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    # Node 1 config
 | 
			
		||||
    cat > cluster1.yaml <<EOF
 | 
			
		||||
node_id: "cluster-1"
 | 
			
		||||
@@ -141,9 +138,8 @@ gossip_interval_max: 10
 | 
			
		||||
sync_interval: 10
 | 
			
		||||
allow_anonymous_read: true
 | 
			
		||||
allow_anonymous_write: true
 | 
			
		||||
cluster_secret: "$CLUSTER_SECRET"
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    # Node 2 config
 | 
			
		||||
    cat > cluster2.yaml <<EOF
 | 
			
		||||
node_id: "cluster-2"
 | 
			
		||||
@@ -157,7 +153,6 @@ gossip_interval_max: 10
 | 
			
		||||
sync_interval: 10
 | 
			
		||||
allow_anonymous_read: true
 | 
			
		||||
allow_anonymous_write: true
 | 
			
		||||
cluster_secret: "$CLUSTER_SECRET"
 | 
			
		||||
EOF
 | 
			
		||||
    
 | 
			
		||||
    # Start nodes
 | 
			
		||||
@@ -235,18 +230,15 @@ EOF
 | 
			
		||||
# but same path. The Merkle tree sync should then trigger conflict resolution.
 | 
			
		||||
test_conflict_resolution() {
 | 
			
		||||
    test_start "Conflict resolution test (Merkle Tree based)"
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    # Create conflicting data using our utility
 | 
			
		||||
    rm -rf conflict1_data conflict2_data 2>/dev/null || true
 | 
			
		||||
    mkdir -p conflict1_data conflict2_data
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    cd "$SCRIPT_DIR"
 | 
			
		||||
    if go run test_conflict.go "$TEST_DIR/conflict1_data" "$TEST_DIR/conflict2_data"; then
 | 
			
		||||
        cd "$TEST_DIR"
 | 
			
		||||
 | 
			
		||||
        # Shared cluster secret for authentication (Issue #13)
 | 
			
		||||
        local CLUSTER_SECRET="conflict-cluster-secret-1234567890123"
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        # Create configs
 | 
			
		||||
        cat > conflict1.yaml <<EOF
 | 
			
		||||
node_id: "conflict-1"
 | 
			
		||||
@@ -258,9 +250,8 @@ log_level: "info"
 | 
			
		||||
sync_interval: 3
 | 
			
		||||
allow_anonymous_read: true
 | 
			
		||||
allow_anonymous_write: true
 | 
			
		||||
cluster_secret: "$CLUSTER_SECRET"
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        cat > conflict2.yaml <<EOF
 | 
			
		||||
node_id: "conflict-2"
 | 
			
		||||
bind_address: "127.0.0.1"
 | 
			
		||||
@@ -271,7 +262,6 @@ log_level: "info"
 | 
			
		||||
sync_interval: 3
 | 
			
		||||
allow_anonymous_read: true
 | 
			
		||||
allow_anonymous_write: true
 | 
			
		||||
cluster_secret: "$CLUSTER_SECRET"
 | 
			
		||||
EOF
 | 
			
		||||
        
 | 
			
		||||
        # Start nodes
 | 
			
		||||
@@ -444,95 +434,6 @@ EOF
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Test 6: Resource Metadata Management (Issue #12)
 | 
			
		||||
test_metadata_management() {
 | 
			
		||||
    test_start "Resource Metadata Management test (Issue #12)"
 | 
			
		||||
 | 
			
		||||
    # Create metadata test config
 | 
			
		||||
    cat > metadata_test.yaml <<EOF
 | 
			
		||||
node_id: "metadata-test"
 | 
			
		||||
bind_address: "127.0.0.1"
 | 
			
		||||
port: 8096
 | 
			
		||||
data_dir: "./metadata_test_data"
 | 
			
		||||
seed_nodes: []
 | 
			
		||||
log_level: "error"
 | 
			
		||||
auth_enabled: true
 | 
			
		||||
allow_anonymous_read: false
 | 
			
		||||
allow_anonymous_write: false
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
    # Start node
 | 
			
		||||
    $BINARY metadata_test.yaml >metadata_test.log 2>&1 &
 | 
			
		||||
    local pid=$!
 | 
			
		||||
 | 
			
		||||
    if wait_for_service 8096; then
 | 
			
		||||
        sleep 2  # Allow root account creation
 | 
			
		||||
 | 
			
		||||
        # Extract the token from logs
 | 
			
		||||
        local token=$(grep "Token:" metadata_test.log | sed 's/.*Token: //' | tr -d '\n\r')
 | 
			
		||||
 | 
			
		||||
        if [ -z "$token" ]; then
 | 
			
		||||
            log_error "Failed to extract authentication token from logs"
 | 
			
		||||
            kill $pid 2>/dev/null || true
 | 
			
		||||
            return
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # First, create a KV resource
 | 
			
		||||
        curl -s -X PUT http://localhost:8096/kv/test/resource -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"data":"test"}' >/dev/null
 | 
			
		||||
        sleep 1
 | 
			
		||||
 | 
			
		||||
        # Test 1: Get metadata should fail for non-existent metadata (initially no metadata exists)
 | 
			
		||||
        local get_response=$(curl -s -w "\n%{http_code}" -X GET http://localhost:8096/kv/test/resource/metadata -H "Authorization: Bearer $token")
 | 
			
		||||
        local get_body=$(echo "$get_response" | head -n -1)
 | 
			
		||||
        local get_code=$(echo "$get_response" | tail -n 1)
 | 
			
		||||
 | 
			
		||||
        if [ "$get_code" = "404" ]; then
 | 
			
		||||
            log_success "GET metadata returns 404 for non-existent metadata"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "GET metadata should return 404 for non-existent metadata, got code: $get_code, body: $get_body"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 2: Update metadata should create new metadata
 | 
			
		||||
        local update_response=$(curl -s -X PUT http://localhost:8096/kv/test/resource/metadata -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"owner_uuid":"test-owner-123","permissions":3840}')
 | 
			
		||||
        if echo "$update_response" | grep -q "owner_uuid"; then
 | 
			
		||||
            log_success "PUT metadata creates metadata successfully"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "PUT metadata should create metadata, got: $update_response"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 3: Get metadata should now return the created metadata
 | 
			
		||||
        local get_response2=$(curl -s -X GET http://localhost:8096/kv/test/resource/metadata -H "Authorization: Bearer $token")
 | 
			
		||||
        if echo "$get_response2" | grep -q "test-owner-123" && echo "$get_response2" | grep -q "3840"; then
 | 
			
		||||
            log_success "GET metadata returns created metadata"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "GET metadata should return created metadata, got: $get_response2"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 4: Update metadata should modify existing metadata
 | 
			
		||||
        local update_response2=$(curl -s -X PUT http://localhost:8096/kv/test/resource/metadata -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"owner_uuid":"new-owner-456"}')
 | 
			
		||||
        if echo "$update_response2" | grep -q "new-owner-456"; then
 | 
			
		||||
            log_success "PUT metadata updates existing metadata"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "PUT metadata should update metadata, got: $update_response2"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 5: Metadata endpoints should require authentication
 | 
			
		||||
        local no_auth=$(curl -s -w "\n%{http_code}" -X GET http://localhost:8096/kv/test/resource/metadata)
 | 
			
		||||
        local no_auth_code=$(echo "$no_auth" | tail -n 1)
 | 
			
		||||
        if [ "$no_auth_code" = "401" ]; then
 | 
			
		||||
            log_success "Metadata endpoints properly require authentication"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "Metadata endpoints should require authentication, got code: $no_auth_code"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        kill $pid 2>/dev/null || true
 | 
			
		||||
        sleep 2
 | 
			
		||||
    else
 | 
			
		||||
        log_error "Metadata test node failed to start"
 | 
			
		||||
        kill $pid 2>/dev/null || true
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Main test execution
 | 
			
		||||
main() {
 | 
			
		||||
    echo "=================================================="
 | 
			
		||||
@@ -551,8 +452,7 @@ main() {
 | 
			
		||||
    test_cluster_formation
 | 
			
		||||
    test_conflict_resolution
 | 
			
		||||
    test_authentication_middleware
 | 
			
		||||
    test_metadata_management
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    # Results
 | 
			
		||||
    echo "=================================================="
 | 
			
		||||
    echo "              Test Results"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.go
									
									
									
									
									
								
							@@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"kvs/server"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	configPath := "./config.yaml"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ import (
 | 
			
		||||
	"kvs/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// healthHandler returns server health status
 | 
			
		||||
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	mode := s.getMode()
 | 
			
		||||
@@ -213,104 +215,6 @@ func (s *Server) deleteKVHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	s.logger.WithField("path", path).Info("Value deleted")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getResourceMetadataHandler retrieves metadata for a KV resource
 | 
			
		||||
func (s *Server) getResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	path := vars["path"]
 | 
			
		||||
 | 
			
		||||
	// Get metadata from storage
 | 
			
		||||
	metadata, err := s.authService.GetResourceMetadata(path)
 | 
			
		||||
	if err == badger.ErrKeyNotFound {
 | 
			
		||||
		http.Error(w, "Not Found: No metadata exists for this resource", http.StatusNotFound)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := types.GetResourceMetadataResponse{
 | 
			
		||||
		OwnerUUID:   metadata.OwnerUUID,
 | 
			
		||||
		GroupUUID:   metadata.GroupUUID,
 | 
			
		||||
		Permissions: metadata.Permissions,
 | 
			
		||||
		TTL:         metadata.TTL,
 | 
			
		||||
		CreatedAt:   metadata.CreatedAt,
 | 
			
		||||
		UpdatedAt:   metadata.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	json.NewEncoder(w).Encode(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateResourceMetadataHandler updates metadata for a KV resource
 | 
			
		||||
func (s *Server) updateResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	path := vars["path"]
 | 
			
		||||
 | 
			
		||||
	// Parse request body
 | 
			
		||||
	var req types.UpdateResourceMetadataRequest
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
		http.Error(w, "Bad Request: Invalid JSON", http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get existing metadata or create new one
 | 
			
		||||
	metadata, err := s.authService.GetResourceMetadata(path)
 | 
			
		||||
	if err == badger.ErrKeyNotFound {
 | 
			
		||||
		// Create new metadata with defaults
 | 
			
		||||
		metadata = &types.ResourceMetadata{
 | 
			
		||||
			OwnerUUID:   "",
 | 
			
		||||
			GroupUUID:   "",
 | 
			
		||||
			Permissions: types.DefaultPermissions,
 | 
			
		||||
			TTL:         "",
 | 
			
		||||
			CreatedAt:   time.Now().Unix(),
 | 
			
		||||
			UpdatedAt:   time.Now().Unix(),
 | 
			
		||||
		}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update only provided fields
 | 
			
		||||
	if req.OwnerUUID != nil {
 | 
			
		||||
		metadata.OwnerUUID = *req.OwnerUUID
 | 
			
		||||
	}
 | 
			
		||||
	if req.GroupUUID != nil {
 | 
			
		||||
		metadata.GroupUUID = *req.GroupUUID
 | 
			
		||||
	}
 | 
			
		||||
	if req.Permissions != nil {
 | 
			
		||||
		metadata.Permissions = *req.Permissions
 | 
			
		||||
	}
 | 
			
		||||
	metadata.UpdatedAt = time.Now().Unix()
 | 
			
		||||
 | 
			
		||||
	// Store updated metadata
 | 
			
		||||
	if err := s.authService.SetResourceMetadata(path, metadata); err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to update resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := types.GetResourceMetadataResponse{
 | 
			
		||||
		OwnerUUID:   metadata.OwnerUUID,
 | 
			
		||||
		GroupUUID:   metadata.GroupUUID,
 | 
			
		||||
		Permissions: metadata.Permissions,
 | 
			
		||||
		TTL:         metadata.TTL,
 | 
			
		||||
		CreatedAt:   metadata.CreatedAt,
 | 
			
		||||
		UpdatedAt:   metadata.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	json.NewEncoder(w).Encode(response)
 | 
			
		||||
 | 
			
		||||
	s.logger.WithFields(logrus.Fields{
 | 
			
		||||
		"path":       path,
 | 
			
		||||
		"owner_uuid": metadata.OwnerUUID,
 | 
			
		||||
		"group_uuid": metadata.GroupUUID,
 | 
			
		||||
	}).Info("Resource metadata updated")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isClusterMember checks if request is from a cluster member
 | 
			
		||||
func (s *Server) isClusterMember(remoteAddr string) bool {
 | 
			
		||||
	host, _, err := net.SplitHostPort(remoteAddr)
 | 
			
		||||
@@ -1368,28 +1272,141 @@ func (s *Server) getSpecificRevision(key string, revision int) (*types.StoredVal
 | 
			
		||||
	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)
 | 
			
		||||
// getResourceMetadataHandler retrieves metadata for a resource path
 | 
			
		||||
func (s *Server) getResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	path := vars["path"]
 | 
			
		||||
 | 
			
		||||
	authCtx := auth.GetAuthContext(r.Context())
 | 
			
		||||
	if authCtx == nil {
 | 
			
		||||
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
 | 
			
		||||
		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)
 | 
			
		||||
	// Check read permission on the resource
 | 
			
		||||
	if !s.authService.CheckResourcePermission(authCtx, path, "read") {
 | 
			
		||||
		http.Error(w, "Forbidden", http.StatusForbidden)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Return the cluster secret for secure bootstrap
 | 
			
		||||
	response := map[string]string{
 | 
			
		||||
		"cluster_secret": s.config.ClusterSecret,
 | 
			
		||||
	metadata, err := s.authService.GetResourceMetadata(path)
 | 
			
		||||
	if err == badger.ErrKeyNotFound {
 | 
			
		||||
		// Return default metadata if not found
 | 
			
		||||
		defaultMetadata := types.ResourceMetadata{
 | 
			
		||||
			OwnerUUID:   authCtx.UserUUID,
 | 
			
		||||
			GroupUUID:   "",
 | 
			
		||||
			Permissions: types.DefaultPermissions,
 | 
			
		||||
			CreatedAt:   time.Now().Unix(),
 | 
			
		||||
			UpdatedAt:   time.Now().Unix(),
 | 
			
		||||
		}
 | 
			
		||||
		metadata = &defaultMetadata
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.logger.WithField("remote_addr", r.RemoteAddr).Info("Cluster secret retrieved for bootstrap")
 | 
			
		||||
	response := types.GetResourceMetadataResponse{
 | 
			
		||||
		OwnerUUID:   metadata.OwnerUUID,
 | 
			
		||||
		GroupUUID:   metadata.GroupUUID,
 | 
			
		||||
		Permissions: metadata.Permissions,
 | 
			
		||||
		TTL:         metadata.TTL,
 | 
			
		||||
		CreatedAt:   metadata.CreatedAt,
 | 
			
		||||
		UpdatedAt:   metadata.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	json.NewEncoder(w).Encode(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateResourceMetadataHandler updates metadata for a resource path
 | 
			
		||||
func (s *Server) updateResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	path := vars["path"]
 | 
			
		||||
 | 
			
		||||
	authCtx := auth.GetAuthContext(r.Context())
 | 
			
		||||
	if authCtx == nil {
 | 
			
		||||
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check write permission on the resource (owner write required for metadata changes)
 | 
			
		||||
	if !s.authService.CheckResourcePermission(authCtx, path, "write") {
 | 
			
		||||
		http.Error(w, "Forbidden", http.StatusForbidden)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req types.UpdateResourceMetadataRequest
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
		http.Error(w, "Bad Request", http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get current metadata (or default if not exists)
 | 
			
		||||
	currentMetadata, err := s.authService.GetResourceMetadata(path)
 | 
			
		||||
	if err == badger.ErrKeyNotFound {
 | 
			
		||||
		currentMetadata = &types.ResourceMetadata{
 | 
			
		||||
			OwnerUUID:   authCtx.UserUUID,
 | 
			
		||||
			GroupUUID:   "",
 | 
			
		||||
			Permissions: types.DefaultPermissions,
 | 
			
		||||
			CreatedAt:   time.Now().Unix(),
 | 
			
		||||
			UpdatedAt:   time.Now().Unix(),
 | 
			
		||||
		}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to get current resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Apply updates only to provided fields
 | 
			
		||||
	updated := false
 | 
			
		||||
	if req.OwnerUUID != "" {
 | 
			
		||||
		currentMetadata.OwnerUUID = req.OwnerUUID
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
	if req.GroupUUID != "" {
 | 
			
		||||
		currentMetadata.GroupUUID = req.GroupUUID
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
	if req.Permissions != 0 {
 | 
			
		||||
		currentMetadata.Permissions = req.Permissions
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
	if req.TTL != "" {
 | 
			
		||||
		currentMetadata.TTL = req.TTL
 | 
			
		||||
		updated = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !updated {
 | 
			
		||||
		http.Error(w, "No fields provided for update", http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Store updated metadata
 | 
			
		||||
	if err := s.authService.StoreResourceMetadata(path, currentMetadata); err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to store resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := types.GetResourceMetadataResponse{
 | 
			
		||||
		OwnerUUID:   currentMetadata.OwnerUUID,
 | 
			
		||||
		GroupUUID:   currentMetadata.GroupUUID,
 | 
			
		||||
		Permissions: currentMetadata.Permissions,
 | 
			
		||||
		TTL:         currentMetadata.TTL,
 | 
			
		||||
		CreatedAt:   currentMetadata.CreatedAt,
 | 
			
		||||
		UpdatedAt:   currentMetadata.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	w.WriteHeader(http.StatusOK)
 | 
			
		||||
	json.NewEncoder(w).Encode(response)
 | 
			
		||||
 | 
			
		||||
	s.logger.WithFields(logrus.Fields{
 | 
			
		||||
		"path":        path,
 | 
			
		||||
		"user_uuid":   authCtx.UserUUID,
 | 
			
		||||
		"owner_uuid":  currentMetadata.OwnerUUID,
 | 
			
		||||
		"group_uuid":  currentMetadata.GroupUUID,
 | 
			
		||||
		"permissions": currentMetadata.Permissions,
 | 
			
		||||
	}).Info("Resource metadata updated")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -13,18 +11,6 @@ func (s *Server) setupRoutes() *mux.Router {
 | 
			
		||||
	// Health endpoint (always available)
 | 
			
		||||
	router.HandleFunc("/health", s.healthHandler).Methods("GET")
 | 
			
		||||
 | 
			
		||||
	// Resource Metadata Management endpoints (Issue #12) - Must come BEFORE general KV routes
 | 
			
		||||
	// These need to be registered first to prevent /kv/{path:.+} from matching metadata paths
 | 
			
		||||
	if s.config.AuthEnabled {
 | 
			
		||||
		router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
 | 
			
		||||
			[]string{"admin:users:read"}, nil, "",
 | 
			
		||||
		)(s.getResourceMetadataHandler)).Methods("GET")
 | 
			
		||||
 | 
			
		||||
		router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
 | 
			
		||||
			[]string{"admin:users:update"}, nil, "",
 | 
			
		||||
		)(s.updateResourceMetadataHandler)).Methods("PUT")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// KV endpoints (with conditional authentication based on anonymous access settings)
 | 
			
		||||
	// GET endpoint - require auth if anonymous read is disabled
 | 
			
		||||
	if s.config.AuthEnabled && !s.config.AllowAnonymousRead {
 | 
			
		||||
@@ -34,7 +20,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(
 | 
			
		||||
@@ -43,7 +29,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(
 | 
			
		||||
@@ -53,34 +39,31 @@ func (s *Server) setupRoutes() *mux.Router {
 | 
			
		||||
		router.HandleFunc("/kv/{path:.+}", s.deleteKVHandler).Methods("DELETE")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Resource Metadata endpoints (available when auth is enabled)
 | 
			
		||||
	if s.config.AuthEnabled {
 | 
			
		||||
		// GET metadata - require read permission
 | 
			
		||||
		router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
 | 
			
		||||
			[]string{"read"}, func(r *http.Request) string { return mux.Vars(r)["path"] }, "read",
 | 
			
		||||
		)(s.getResourceMetadataHandler)).Methods("GET")
 | 
			
		||||
		
 | 
			
		||||
		// PUT metadata - require write permission (owner write)
 | 
			
		||||
		router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
 | 
			
		||||
			[]string{"write"}, func(r *http.Request) string { return mux.Vars(r)["path"] }, "write",
 | 
			
		||||
		)(s.updateResourceMetadataHandler)).Methods("PUT")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Member endpoints (available when clustering is enabled)
 | 
			
		||||
	if s.config.ClusteringEnabled {
 | 
			
		||||
		// GET /members/ is unprotected for monitoring/inspection
 | 
			
		||||
		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 to all cluster communication endpoints
 | 
			
		||||
		if s.clusterAuthService != nil {
 | 
			
		||||
			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.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/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")
 | 
			
		||||
		}
 | 
			
		||||
		// 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)
 | 
			
		||||
@@ -89,15 +72,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")
 | 
			
		||||
@@ -106,15 +89,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")
 | 
			
		||||
@@ -123,12 +106,6 @@ 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -50,8 +50,7 @@ type Server struct {
 | 
			
		||||
	backupMu      sync.RWMutex       // Protects backup status
 | 
			
		||||
 | 
			
		||||
	// Authentication service
 | 
			
		||||
	authService        *auth.AuthService
 | 
			
		||||
	clusterAuthService *auth.ClusterAuthService
 | 
			
		||||
	authService *auth.AuthService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewServer initializes and returns a new Server instance
 | 
			
		||||
@@ -121,11 +120,6 @@ 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 {
 | 
			
		||||
@@ -225,7 +219,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")
 | 
			
		||||
@@ -240,7 +234,7 @@ func (s *Server) createRootUserAndToken() error {
 | 
			
		||||
		UpdatedAt: now,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create root user
 | 
			
		||||
	// Create root user  
 | 
			
		||||
	rootUser := types.User{
 | 
			
		||||
		UUID:         rootUserUUID,
 | 
			
		||||
		NicknameHash: hashUserNickname(rootNickname),
 | 
			
		||||
@@ -257,7 +251,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",
 | 
			
		||||
	}
 | 
			
		||||
@@ -275,13 +269,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")
 | 
			
		||||
@@ -315,7 +309,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)
 | 
			
		||||
		}
 | 
			
		||||
@@ -325,7 +319,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)
 | 
			
		||||
		}
 | 
			
		||||
@@ -333,3 +327,4 @@ func (s *Server) storeUserAndGroup(user *types.User, group *types.Group) error {
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								types/types.go
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								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) |
 | 
			
		||||
@@ -131,7 +131,14 @@ type CreateTokenResponse struct {
 | 
			
		||||
	ExpiresAt int64  `json:"expires_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Resource Metadata Management API structures (Issue #12)
 | 
			
		||||
// Resource Metadata Management API structures
 | 
			
		||||
type UpdateResourceMetadataRequest struct {
 | 
			
		||||
	OwnerUUID   string `json:"owner_uuid,omitempty"`
 | 
			
		||||
	GroupUUID   string `json:"group_uuid,omitempty"`
 | 
			
		||||
	Permissions int    `json:"permissions,omitempty"`
 | 
			
		||||
	TTL         string `json:"ttl,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetResourceMetadataResponse struct {
 | 
			
		||||
	OwnerUUID   string `json:"owner_uuid"`
 | 
			
		||||
	GroupUUID   string `json:"group_uuid"`
 | 
			
		||||
@@ -141,12 +148,6 @@ type GetResourceMetadataResponse struct {
 | 
			
		||||
	UpdatedAt   int64  `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateResourceMetadataRequest struct {
 | 
			
		||||
	OwnerUUID   *string `json:"owner_uuid,omitempty"`
 | 
			
		||||
	GroupUUID   *string `json:"group_uuid,omitempty"`
 | 
			
		||||
	Permissions *int    `json:"permissions,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Cluster and member management types
 | 
			
		||||
type Member struct {
 | 
			
		||||
	ID              string `json:"id"`
 | 
			
		||||
@@ -247,57 +248,50 @@ 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
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,4 +22,4 @@ func HashGroupName(groupname string) string {
 | 
			
		||||
 | 
			
		||||
func HashToken(token string) string {
 | 
			
		||||
	return HashSHA3512(token)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user