forked from ryyst/kalzu-value-store
- Removed all duplicate Server methods from main.go (630 lines) - Fixed import conflicts and unused imports - main.go reduced from 3,298 to 340 lines (89% reduction) - Clean modular structure with server package handling all server functionality - Achieved clean build with no compilation errors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
321 lines
8.8 KiB
Go
321 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v4"
|
|
"github.com/gorilla/mux"
|
|
|
|
"kvs/config"
|
|
"kvs/server"
|
|
"kvs/types"
|
|
"kvs/utils"
|
|
)
|
|
|
|
// Phase 2: Permission checking utilities
|
|
func checkPermission(permissions int, operation string, isOwner, isGroupMember bool) bool {
|
|
switch operation {
|
|
case "create":
|
|
if isOwner {
|
|
return (permissions & types.PermOwnerCreate) != 0
|
|
}
|
|
if isGroupMember {
|
|
return (permissions & types.PermGroupCreate) != 0
|
|
}
|
|
return (permissions & types.PermOthersCreate) != 0
|
|
|
|
case "delete":
|
|
if isOwner {
|
|
return (permissions & types.PermOwnerDelete) != 0
|
|
}
|
|
if isGroupMember {
|
|
return (permissions & types.PermGroupDelete) != 0
|
|
}
|
|
return (permissions & types.PermOthersDelete) != 0
|
|
|
|
case "write":
|
|
if isOwner {
|
|
return (permissions & types.PermOwnerWrite) != 0
|
|
}
|
|
if isGroupMember {
|
|
return (permissions & types.PermGroupWrite) != 0
|
|
}
|
|
return (permissions & types.PermOthersWrite) != 0
|
|
|
|
case "read":
|
|
if isOwner {
|
|
return (permissions & types.PermOwnerRead) != 0
|
|
}
|
|
if isGroupMember {
|
|
return (permissions & types.PermGroupRead) != 0
|
|
}
|
|
return (permissions & types.PermOthersRead) != 0
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Helper function to determine 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 {
|
|
isGroupMember = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return isOwner, isGroupMember
|
|
}
|
|
|
|
// Phase 2: JWT token management utilities
|
|
|
|
// JWT signing key (should be configurable in production)
|
|
var jwtSigningKey = []byte("your-secret-signing-key-change-this-in-production")
|
|
|
|
// JWTClaims represents the custom claims for our JWT tokens
|
|
type JWTClaims struct {
|
|
UserUUID string `json:"user_uuid"`
|
|
Scopes []string `json:"scopes"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// generateJWT creates a new JWT token for a user with specified scopes
|
|
|
|
// validateJWT validates a JWT token and returns the claims if valid
|
|
func validateJWT(tokenString string) (*JWTClaims, error) {
|
|
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
// Validate signing method
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
}
|
|
return jwtSigningKey, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
|
return claims, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("invalid token")
|
|
}
|
|
|
|
// storeAPIToken stores an API token in BadgerDB with TTL
|
|
|
|
// getAPIToken retrieves an API token from BadgerDB by hash
|
|
|
|
// Phase 2: Authorization middleware
|
|
|
|
// AuthContext holds authentication information for a request
|
|
type AuthContext struct {
|
|
UserUUID string `json:"user_uuid"`
|
|
Scopes []string `json:"scopes"`
|
|
Groups []string `json:"groups"`
|
|
}
|
|
|
|
// extractTokenFromHeader extracts the Bearer token from the Authorization header
|
|
func extractTokenFromHeader(r *http.Request) (string, error) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return "", fmt.Errorf("missing authorization header")
|
|
}
|
|
|
|
parts := strings.Split(authHeader, " ")
|
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
|
return "", fmt.Errorf("invalid authorization header format")
|
|
}
|
|
|
|
return parts[1], nil
|
|
}
|
|
|
|
// getUserGroups retrieves all groups that a user belongs to
|
|
|
|
// authenticateRequest validates the JWT token and returns authentication context
|
|
|
|
// checkResourcePermission checks if a user has permission to perform an operation on a resource
|
|
|
|
// authMiddleware is the HTTP middleware that enforces authentication and authorization
|
|
|
|
// Helper function to extract KV resource key from request
|
|
func extractKVResourceKey(r *http.Request) string {
|
|
vars := mux.Vars(r)
|
|
if path, ok := vars["path"]; ok {
|
|
return path
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Phase 2: ZSTD compression utilities
|
|
|
|
// compressData compresses JSON data using storage service
|
|
|
|
// decompressData decompresses data using storage service
|
|
|
|
// Phase 2: TTL and size validation utilities
|
|
|
|
// parseTTL converts a Go duration string to time.Duration
|
|
func parseTTL(ttlString string) (time.Duration, error) {
|
|
if ttlString == "" || ttlString == "0" {
|
|
return 0, nil // No TTL
|
|
}
|
|
|
|
duration, err := time.ParseDuration(ttlString)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid TTL format: %v", err)
|
|
}
|
|
|
|
if duration < 0 {
|
|
return 0, fmt.Errorf("TTL cannot be negative")
|
|
}
|
|
|
|
return duration, nil
|
|
}
|
|
|
|
// validateJSONSize checks if JSON data exceeds maximum allowed size
|
|
|
|
// createResourceMetadata creates metadata for a new resource with TTL and permissions
|
|
|
|
// storeWithTTL stores data in BadgerDB with optional TTL
|
|
|
|
// retrieveWithDecompression retrieves and decompresses data from BadgerDB
|
|
|
|
// Phase 2: Revision history system utilities
|
|
|
|
// getRevisionKey generates the storage key for a specific revision
|
|
func getRevisionKey(baseKey string, revision int) string {
|
|
return fmt.Sprintf("%s:rev:%d", baseKey, revision)
|
|
}
|
|
|
|
// storeRevisionHistory stores a value and manages revision history (up to 3 revisions)
|
|
func getRateLimitKey(userUUID string, windowStart int64) string {
|
|
return fmt.Sprintf("ratelimit:%s:%d", userUUID, windowStart)
|
|
}
|
|
|
|
// getCurrentWindow calculates the current rate limiting window start time
|
|
|
|
// checkRateLimit checks if a user has exceeded the rate limit
|
|
|
|
// rateLimitMiddleware is the HTTP middleware that enforces rate limiting
|
|
|
|
// Phase 2: Tamper-evident logging utilities
|
|
|
|
// getTamperLogKey generates the storage key for a tamper log entry
|
|
func getTamperLogKey(timestamp string, entryUUID string) string {
|
|
return fmt.Sprintf("log:%s:%s", timestamp, entryUUID)
|
|
}
|
|
|
|
// getMerkleLogKey generates the storage key for hourly Merkle tree roots
|
|
func getMerkleLogKey(timestamp string) string {
|
|
return fmt.Sprintf("log:merkle:%s", timestamp)
|
|
}
|
|
|
|
// generateLogSignature creates a SHA3-512 signature for a log entry
|
|
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)
|
|
}
|
|
|
|
// isActionLogged checks if a specific action should be logged
|
|
|
|
// createTamperLogEntry creates a new tamper-evident log entry
|
|
|
|
// storeTamperLogEntry stores a tamper-evident log entry in BadgerDB
|
|
|
|
// logTamperEvent logs a tamper-evident event if the action is configured for logging
|
|
|
|
// getTamperLogs retrieves tamper log entries within a time range (for auditing)
|
|
|
|
// Phase 2: Backup system utilities
|
|
|
|
// 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"))
|
|
}
|
|
|
|
// createBackup creates a compressed backup of the BadgerDB database
|
|
|
|
// cleanupOldBackups removes backup files older than the retention period
|
|
|
|
// initializeBackupScheduler sets up the cron scheduler for automated backups
|
|
|
|
// getBackupStatus returns the current backup status
|
|
|
|
// Initialize server
|
|
|
|
// performMerkleSync performs a synchronization round using Merkle Trees
|
|
|
|
// requestMerkleRoot requests the Merkle root from a peer
|
|
|
|
// diffMerkleTreesRecursive recursively compares local and remote Merkle tree nodes
|
|
|
|
// requestMerkleDiff requests children hashes or keys for a given node/range from a peer
|
|
|
|
// fetchSingleKVFromPeer fetches a single KV pair from a peer
|
|
|
|
// storeReplicatedDataWithMetadata stores replicated data preserving its original metadata
|
|
|
|
// deleteKVLocally deletes a key-value pair and its associated timestamp index locally.
|
|
|
|
// fetchAndStoreRange fetches a range of KV pairs from a peer and stores them locally
|
|
|
|
// Bootstrap - join cluster using seed nodes
|
|
|
|
// Attempt to join cluster via a seed node
|
|
|
|
// Perform gradual sync (Merkle-based version)
|
|
|
|
// Resolve conflict between local and remote data using majority vote and oldest node tie-breaker
|
|
|
|
// Resolve conflict using oldest node rule when no other members available
|
|
|
|
// getLocalData is a utility to retrieve a types.StoredValue from local DB.
|
|
|
|
func main() {
|
|
configPath := "./config.yaml"
|
|
|
|
// Simple CLI argument parsing
|
|
if len(os.Args) > 1 {
|
|
configPath = os.Args[1]
|
|
}
|
|
|
|
cfg, err := config.Load(configPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
kvServer, err := server.NewServer(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create server: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Handle graceful shutdown
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
<-sigCh
|
|
kvServer.Stop()
|
|
}()
|
|
|
|
if err := kvServer.Start(); err != nil && err != http.ErrServerClosed {
|
|
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|