package raft import ( "crypto/hmac" "crypto/sha1" "crypto/sha256" "encoding/base32" "encoding/binary" "encoding/hex" "encoding/json" "fmt" "strconv" "strings" "sync" "time" "igit.com/xbase/raft/db" ) // ==================== Auth Constants ==================== const ( ActionRead = "read" ActionWrite = "write" // Includes Set, Del ActionAdmin = "admin" // Cluster ops SystemKeyPrefix = "system." AuthUserPrefix = "system.user." AuthRolePrefix = "system.role." AuthSessionPrefix = "system.session." AuthConfigKey = "system.config" ) // ==================== Auth Data Structures ==================== // Constraint defines numeric constraints on values type Constraint struct { Min *float64 `json:"min,omitempty"` Max *float64 `json:"max,omitempty"` } // Permission defines access to a key pattern type Permission struct { KeyPattern string `json:"key"` // Prefix match. "*" means all. Actions []string `json:"actions"` // "read", "write", "admin" Constraint *Constraint `json:"constraint,omitempty"` } // Role defines a set of permissions type Role struct { Name string `json:"name"` Permissions []Permission `json:"permissions"` ParentRoles []string `json:"parent_roles,omitempty"` // Inherit permissions from these roles } // User defines a system user type User struct { Username string `json:"username"` PasswordHash string `json:"password_hash"` // SHA256(salt + password) Salt string `json:"salt"` Roles []string `json:"roles"` // Specific overrides AllowPermissions []Permission `json:"allow_permissions"` DenyPermissions []Permission `json:"deny_permissions"` // Higher priority Email string `json:"email"` Icon string `json:"icon"` IsOnline bool `json:"is_online"` MFASecret string `json:"mfa_secret"` // Base32 encoded TOTP secret MFAEnabled bool `json:"mfa_enabled"` WhitelistIPs []string `json:"whitelist_ips"` BlacklistIPs []string `json:"blacklist_ips"` } // Session represents an active login session // Modified: Sessions are stored in Raft, but permCache is transient type Session struct { Token string `json:"token"` Username string `json:"username"` LoginIP string `json:"login_ip"` Expiry int64 `json:"expiry"` // Unix timestamp // Permission Cache: key+action -> bool // Used to speed up frequent checks (e.g. loops) // Not serialized to JSON (rebuilt on demand) permCache sync.Map `json:"-"` } // AuthConfig defines global auth settings type AuthConfig struct { Enabled bool `json:"enabled"` } // ==================== Auth Manager ==================== // AuthManager handles authentication and authorization type AuthManager struct { server *KVServer mu sync.RWMutex // In-memory cache users map[string]*User roles map[string]*Role // sessions are now ONLY in-memory (local cache) sessions map[string]*Session enabled bool } // NewAuthManager creates a new AuthManager func NewAuthManager(server *KVServer) *AuthManager { return &AuthManager{ server: server, users: make(map[string]*User), roles: make(map[string]*Role), sessions: make(map[string]*Session), enabled: false, } } // HashPassword hashes a password with salt (Exported for tools) func HashPassword(password, salt string) string { hash := sha256.Sum256([]byte(salt + password)) return hex.EncodeToString(hash[:]) } // Internal helper func hashPassword(password, salt string) string { return HashPassword(password, salt) } // Helper to generate token func generateToken() string { // In a real system use crypto/rand, but here we use math/rand seeded in main or just simple timestamp mix for example // Since we can't easily access crypto/rand without import and "no external lib" usually implies standard lib only, // crypto/rand IS standard lib. h := sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) return hex.EncodeToString(h[:]) } // Helper for TOTP (Google Authenticator) func validateTOTP(secret string, code string) bool { // Decode base32 secret key, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret)) if err != nil { return false } // Current time steps (30s window) // Check current, prev, and next window to allow for slight skew now := time.Now().Unix() return checkTOTP(key, now, code) || checkTOTP(key, now-30, code) || checkTOTP(key, now+30, code) } func checkTOTP(key []byte, timestamp int64, code string) bool { step := timestamp / 30 buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, uint64(step)) h := hmac.New(sha1.New, key) h.Write(buf) sum := h.Sum(nil) offset := sum[len(sum)-1] & 0xf val := int64(((int(sum[offset]) & 0x7f) << 24) | ((int(sum[offset+1]) & 0xff) << 16) | ((int(sum[offset+2]) & 0xff) << 8) | (int(sum[offset+3]) & 0xff)) otp := val % 1000000 expected := fmt.Sprintf("%06d", otp) return expected == code } // UpdateCache updates the internal cache based on key-value changes // Called by the state machine when applying logs func (am *AuthManager) UpdateCache(key, value string, isDelete bool) { am.mu.Lock() defer am.mu.Unlock() if key == AuthConfigKey { if isDelete { am.enabled = false } else { var config AuthConfig if err := json.Unmarshal([]byte(value), &config); err == nil { am.enabled = config.Enabled } } return } if strings.HasPrefix(key, AuthUserPrefix) { username := strings.TrimPrefix(key, AuthUserPrefix) if isDelete { delete(am.users, username) } else { var user User if err := json.Unmarshal([]byte(value), &user); err == nil { am.users[username] = &user am.server.Raft.config.Logger.Info("Updated user cache for %s (hash prefix: %s)", username, user.PasswordHash[:8]) } else { am.server.Raft.config.Logger.Error("Failed to unmarshal user %s: %v", username, err) } } return } if strings.HasPrefix(key, AuthRolePrefix) { rolename := strings.TrimPrefix(key, AuthRolePrefix) if isDelete { delete(am.roles, rolename) } else { var role Role if err := json.Unmarshal([]byte(value), &role); err == nil { am.roles[rolename] = &role } } return } // Note: AuthSessionPrefix is no longer handled here because sessions are local only. if strings.HasPrefix(key, AuthSessionPrefix) { token := strings.TrimPrefix(key, AuthSessionPrefix) if isDelete { delete(am.sessions, token) } else { var session Session if err := json.Unmarshal([]byte(value), &session); err == nil { // Check expiry (though normally we'd replicate delete on expiry, but follower can check too) if time.Now().Unix() > session.Expiry { delete(am.sessions, token) } else { am.sessions[token] = &session } } } return } // Invalidate permission cache on any auth change (Users, Roles, or Sessions changed) // Even though session changes are frequent, for safety/simplicity we can clear cache. // But optimizing: Session change only affects THAT session. // User/Role change affects potentially all sessions. // Let's clear cache if User/Role changed. // If Session changed (added/removed), CheckPermission will hit/miss am.sessions map anyway. // So we don't strictly need to clear permCache for session changes unless we cache existence. // We cache (token:key:action -> result). // If session is deleted, we should clear its cache entries? // The sync.Map doesn't support easy clearing by pattern. // Given we want High Performance for search (loops), cache is useful. // But if we use "Cluster Shared Session", updates come via Raft. // Let's just clear everything on User/Role update. // For Session update, we don't clear global cache, relying on GetSession check in CheckPermission. } // IsEnabled checks if auth is enabled func (am *AuthManager) IsEnabled() bool { am.mu.RLock() defer am.mu.RUnlock() return am.enabled } // CheckPermission checks if a user has permission to perform an action on a key func (am *AuthManager) CheckPermission(token string, key string, action string, value string) error { am.mu.RLock() enabled := am.enabled am.mu.RUnlock() if !enabled { return nil } // Internal system bypass if token == "SYSTEM_INTERNAL" { return nil } am.mu.RLock() session, ok := am.sessions[token] am.mu.RUnlock() if !ok { return fmt.Errorf("invalid or expired session") } if time.Now().Unix() > session.Expiry { return fmt.Errorf("session expired") } // Check Session Local Cache // Cache key: "action:key" (value is harder to cache due to constraints, but if value is empty/read, we can cache) // For writes with value constraints, caching is risky unless we include value in cache key or don't cache constraint checks. // For READS (action="read"), value is empty, so we can cache. // For WRITES without value (Del), or simple sets, we can cache if no constraints. // Let's safe cache only for Read or if we verify constraint logic is stable. // To be safe and simple: Cache the result of (key, action). If constraints exist, the checkPerms function will fail if value invalid. // But if we cache "true", we skip checkPerms. // So we can only cache if the permission granted did NOT depend on value constraints. // That's complex. // Simplified: Cache only "read" and "admin" actions? cacheKey := action + ":" + key if action == ActionRead { if val, hit := session.permCache.Load(cacheKey); hit { if allowed, ok := val.(bool); ok { if allowed { return nil } else { return fmt.Errorf("permission denied (cached)") } } } } am.mu.RLock() user, userOk := am.users[session.Username] am.mu.RUnlock() if !userOk { return fmt.Errorf("user not found") } err := am.checkUserPermission(user, key, action, value) // Update Cache for Read if action == ActionRead { session.permCache.Store(cacheKey, err == nil) } return err } func (am *AuthManager) checkUserPermission(user *User, key, action, value string) error { // 1. Check Deny Permissions (User level) for _, perm := range user.DenyPermissions { if matchKey(perm.KeyPattern, key) && hasAction(perm.Actions, action) { return fmt.Errorf("permission denied (explicit deny)") } } // 2. Check Allow Permissions (User level) if checkPerms(user.AllowPermissions, key, action, value) { return nil } // 3. Check Roles for _, roleName := range user.Roles { if am.checkRole(roleName, key, action, value, make(map[string]bool)) { return nil } } return fmt.Errorf("permission denied") } func (am *AuthManager) checkRole(roleName string, key, action, value string, visited map[string]bool) bool { if visited[roleName] { return false } visited[roleName] = true am.mu.RLock() role, ok := am.roles[roleName] am.mu.RUnlock() if !ok { return false } if checkPerms(role.Permissions, key, action, value) { return true } // Check parent roles for _, parent := range role.ParentRoles { if am.checkRole(parent, key, action, value, visited) { return true } } return false } func matchKey(pattern, key string) bool { if pattern == "*" { return true } if pattern == key { return true } // Prefix match: "user.asin" matches "user.asin.china" if strings.HasSuffix(pattern, "*") { prefix := strings.TrimSuffix(pattern, "*") return strings.HasPrefix(key, prefix) } // Hierarchy match: pattern "a.b" matches "a.b.c" if strings.HasPrefix(key, pattern+".") { return true } return false } func hasAction(actions []string, target string) bool { for _, a := range actions { if a == target || a == "*" { return true } } return false } func checkPerms(perms []Permission, key, action, value string) bool { for _, perm := range perms { if matchKey(perm.KeyPattern, key) && hasAction(perm.Actions, action) { // Check Value Constraints if action is write if action == ActionWrite && perm.Constraint != nil && value != "" { // Try to parse value as float val, err := strconv.ParseFloat(value, 64) if err == nil { if perm.Constraint.Min != nil && val < *perm.Constraint.Min { return false } if perm.Constraint.Max != nil && val > *perm.Constraint.Max { return false } } else { // If value is not a number but constraint exists, what to do? // Strict mode: fail. return false } } return true } } return false } // Login authenticates a user and returns a token // Modified: Sessions are stored in RAFT (Cluster Shared) func (am *AuthManager) Login(username, password, totpCode, ip string) (string, error) { am.mu.RLock() if !am.enabled { am.mu.RUnlock() return "", fmt.Errorf("auth not enabled") } user, ok := am.users[username] am.mu.RUnlock() if !ok { return "", fmt.Errorf("invalid credentials") } // Check IP if len(user.WhitelistIPs) > 0 { allowed := false for _, w := range user.WhitelistIPs { if w == ip { allowed = true break } } if !allowed { return "", fmt.Errorf("ip not allowed") } } if len(user.BlacklistIPs) > 0 { for _, b := range user.BlacklistIPs { if b == ip { return "", fmt.Errorf("ip blacklisted") } } } // Verify Password if hashPassword(password, user.Salt) != user.PasswordHash { return "", fmt.Errorf("invalid credentials") } // Verify TOTP if user.MFAEnabled { if totpCode == "" { return "", fmt.Errorf("mfa code required") } if !validateTOTP(user.MFASecret, totpCode) { return "", fmt.Errorf("invalid mfa code") } } // Create Session token := generateToken() session := &Session{ Token: token, Username: username, LoginIP: ip, Expiry: time.Now().Add(24 * time.Hour).Unix(), } // Store session via Raft (Propose) data, _ := json.Marshal(session) if err := am.server.Set(AuthSessionPrefix+token, string(data)); err != nil { return "", err } return token, nil } // Logout invalidates a session (Cluster Shared) func (am *AuthManager) Logout(token string) error { return am.server.Del(AuthSessionPrefix + token) } // GetSession returns session info func (am *AuthManager) GetSession(token string) (*Session, error) { am.mu.RLock() defer am.mu.RUnlock() if token == "SYSTEM_INTERNAL" { return &Session{Username: "system"}, nil } session, ok := am.sessions[token] if !ok { return nil, fmt.Errorf("invalid session") } if time.Now().Unix() > session.Expiry { return nil, fmt.Errorf("session expired") } return session, nil } // GetUser returns user info (Public for internal use) func (am *AuthManager) GetUser(username string) (*User, error) { am.mu.RLock() defer am.mu.RUnlock() user, ok := am.users[username] if !ok { return nil, fmt.Errorf("user not found") } return user, nil } // RegisterUser creates a new user func (am *AuthManager) RegisterUser(username, password string, roles []string) error { am.mu.RLock() if _, exists := am.users[username]; exists { am.mu.RUnlock() return fmt.Errorf("user already exists") } am.mu.RUnlock() salt := fmt.Sprintf("%d", time.Now().UnixNano()) // Simple salt user := User{ Username: username, Salt: salt, PasswordHash: hashPassword(password, salt), Roles: roles, } data, _ := json.Marshal(user) // Use SetSync to ensure user is created before returning return am.server.SetSync(AuthUserPrefix+username, string(data)) } // ChangePassword updates a user's password func (am *AuthManager) ChangePassword(username, newPassword string) error { am.mu.RLock() user, exists := am.users[username] am.mu.RUnlock() if !exists { return fmt.Errorf("user not found") } newUser := *user // Shallow copy to modify newUser.Salt = fmt.Sprintf("%d", time.Now().UnixNano()) newUser.PasswordHash = hashPassword(newPassword, newUser.Salt) data, _ := json.Marshal(newUser) // Use SetSync to ensure password is updated before returning return am.server.SetSync(AuthUserPrefix+username, string(data)) } // UpdateUser updates generic fields of a user (including roles) func (am *AuthManager) UpdateUser(user User) error { am.mu.RLock() _, exists := am.users[user.Username] am.mu.RUnlock() if !exists { return fmt.Errorf("user not found") } // Ensure we don't overwrite passwordhash/salt blindly if they are empty // But usually UpdateUser is called with a fully populated user object from GetUser + mods // So we just save it. data, _ := json.Marshal(user) return am.server.SetSync(AuthUserPrefix+user.Username, string(data)) } // CreateRole creates a new role func (am *AuthManager) CreateRole(name string) error { am.mu.RLock() if _, exists := am.roles[name]; exists { am.mu.RUnlock() return fmt.Errorf("role already exists") } am.mu.RUnlock() role := Role{Name: name} data, _ := json.Marshal(role) return am.server.SetSync(AuthRolePrefix+name, string(data)) } // DeleteRole deletes a role func (am *AuthManager) DeleteRole(name string) error { am.mu.RLock() if _, exists := am.roles[name]; !exists { am.mu.RUnlock() return fmt.Errorf("role not found") } am.mu.RUnlock() return am.server.DelSync(AuthRolePrefix + name) } // UpdateRole updates an existing role func (am *AuthManager) UpdateRole(role Role) error { am.mu.RLock() if _, exists := am.roles[role.Name]; !exists { am.mu.RUnlock() return fmt.Errorf("role not found") } am.mu.RUnlock() data, _ := json.Marshal(role) return am.server.SetSync(AuthRolePrefix+role.Name, string(data)) } // GetRole returns a role definition func (am *AuthManager) GetRole(name string) (*Role, error) { am.mu.RLock() defer am.mu.RUnlock() role, ok := am.roles[name] if !ok { return nil, fmt.Errorf("role not found") } return role, nil } // ListUsers lists all users func (am *AuthManager) ListUsers() []*User { am.mu.RLock() defer am.mu.RUnlock() users := make([]*User, 0, len(am.users)) for _, u := range am.users { users = append(users, u) } return users } // ListRoles lists all roles func (am *AuthManager) ListRoles() []*Role { am.mu.RLock() defer am.mu.RUnlock() roles := make([]*Role, 0, len(am.roles)) for _, r := range am.roles { roles = append(roles, r) } return roles } // HasFullAccess checks if the user has "*" permission on all keys (superuser) // allowing optimizations like pushing limits to DB engine. func (am *AuthManager) HasFullAccess(token string) bool { am.mu.RLock() defer am.mu.RUnlock() if !am.enabled { return true } if token == "SYSTEM_INTERNAL" { return true } session, ok := am.sessions[token] if !ok || time.Now().Unix() > session.Expiry { return false } user, ok := am.users[session.Username] if !ok { return false } // Must not have any deny permissions that could block "*" // Actually if Deny is empty, and Allow has "*", it's full access. // If Deny has something, we can't assume full access blindly. if len(user.DenyPermissions) > 0 { return false } // Check Allow for _, perm := range user.AllowPermissions { if perm.KeyPattern == "*" && hasAction(perm.Actions, "*") { return true } } // Check Roles // This is recursive, so we might skip deeply checking roles for optimization simplicity // and only support direct "*" assignment for this optimization. // Or we can quickly check if any role has "*" for _, roleName := range user.Roles { if am.checkRoleFullAccess(roleName, make(map[string]bool)) { return true } } return false } func (am *AuthManager) checkRoleFullAccess(roleName string, visited map[string]bool) bool { if visited[roleName] { return false } visited[roleName] = true role, ok := am.roles[roleName] if !ok { return false } for _, perm := range role.Permissions { if perm.KeyPattern == "*" && hasAction(perm.Actions, "*") { return true } } for _, parent := range role.ParentRoles { if am.checkRoleFullAccess(parent, visited) { return true } } return false } // Admin helpers to bootstrap func (am *AuthManager) CreateRootUser(password string) error { salt := "somesalt" // In production use random user := User{ Username: "root", Salt: salt, PasswordHash: hashPassword(password, salt), AllowPermissions: []Permission{ {KeyPattern: "*", Actions: []string{"*"}}, }, } data, _ := json.Marshal(user) return am.server.Set(AuthUserPrefix+"root", string(data)) } func (am *AuthManager) EnableAuth() error { config := AuthConfig{Enabled: true} data, _ := json.Marshal(config) return am.server.Set(AuthConfigKey, string(data)) } func (am *AuthManager) DisableAuth() error { // Only admin can do this via normal SetAuthenticated config := AuthConfig{Enabled: false} data, _ := json.Marshal(config) return am.server.Set(AuthConfigKey, string(data)) } // LoadFromDB loads auth data from existing DB func (am *AuthManager) LoadFromDB() error { prefix := SystemKeyPrefix // We use the DB engine's index to find all system keys am.server.DB.Index.WalkPrefix(prefix, func(key string, entry db.IndexEntry) bool { // Read value from storage // We need to access Storage via Engine. // Engine.Storage is exported. val, err := am.server.DB.Storage.ReadValue(entry.ValueOffset) if err != nil { // Skip corrupted or missing values return true } // Update Cache // Note: We are essentially replaying the state into the cache am.UpdateCache(key, val, false) return true }) return nil }