feat: enforce tenant login IP whitelists

This commit is contained in:
richjohnrambo
2026-09-09 09:40:37 +08:00
parent 9ac2b412cc
commit 92a72d1bea
4 changed files with 198 additions and 2 deletions
+88
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/tinode/chat/server/auth"
@@ -15,6 +16,18 @@ import (
"github.com/tinode/chat/server/store/types"
)
const tenantLoginIPWhitelistCacheTTL = 30 * time.Second
type tenantLoginIPWhitelistCacheEntry struct {
config tenantLoginIPWhitelistConfig
expiresAt time.Time
}
var tenantLoginIPWhitelistCache = struct {
sync.RWMutex
items map[types.TenantID]tenantLoginIPWhitelistCacheEntry
}{items: make(map[types.TenantID]tenantLoginIPWhitelistCacheEntry)}
func recordSessionLoginSuccess(s *Session, rec *auth.Rec, scheme, suppliedAccount, requestID string, at time.Time) {
if s == nil || rec == nil || rec.Uid.IsZero() || !s.tenantID.IsValid() {
return
@@ -102,6 +115,81 @@ func checkUserLoginIPWhitelist(tenantID types.TenantID, uid types.Uid, remoteAdd
return types.ErrPermissionDenied
}
func checkTenantLoginIPWhitelist(tenantID types.TenantID, platform, remoteAddr string) error {
config, err := loadTenantLoginIPWhitelist(tenantID)
if err != nil {
return err
}
if config == nil {
return nil
}
values := config.AppList
if tenantLoginIPChannel(platform) == "web" {
values = config.WebList
}
if len(values) == 0 {
return nil
}
if whitelistContainsIP(values, sessionAuditIP(strings.Split(remoteAddr, ",")[0])) {
return nil
}
return types.ErrPermissionDenied
}
func loadTenantLoginIPWhitelist(tenantID types.TenantID) (*tenantLoginIPWhitelistConfig, error) {
if loginIPWhitelistProvider == nil {
return nil, nil
}
now := time.Now()
tenantLoginIPWhitelistCache.RLock()
cached, ok := tenantLoginIPWhitelistCache.items[tenantID]
tenantLoginIPWhitelistCache.RUnlock()
if ok && now.Before(cached.expiresAt) {
result := cached.config
return &result, nil
}
config, err := loginIPWhitelistProvider.TenantLoginIPWhitelist(tenantID)
if err != nil {
if ok {
result := cached.config
return &result, nil
}
return nil, err
}
if config == nil {
config = &tenantLoginIPWhitelistConfig{TenantID: tenantID, AppList: []string{}, WebList: []string{}}
}
tenantLoginIPWhitelistCache.Lock()
tenantLoginIPWhitelistCache.items[tenantID] = tenantLoginIPWhitelistCacheEntry{config: *config, expiresAt: now.Add(tenantLoginIPWhitelistCacheTTL)}
tenantLoginIPWhitelistCache.Unlock()
result := *config
return &result, nil
}
func tenantLoginIPChannel(platform string) string {
if strings.EqualFold(strings.TrimSpace(platform), "web") {
return "web"
}
return "app"
}
func whitelistContainsIP(values []string, ipText string) bool {
ip := net.ParseIP(strings.TrimSpace(ipText))
if ip == nil {
return false
}
for _, value := range values {
value = strings.TrimSpace(value)
if allowedIP := net.ParseIP(value); allowedIP != nil && allowedIP.Equal(ip) {
return true
}
if _, block, err := net.ParseCIDR(value); err == nil && block.Contains(ip) {
return true
}
}
return false
}
func loginAuditAccount(scheme, supplied, credential string) (string, string) {
if parts := strings.SplitN(strings.TrimSpace(credential), ":", 2); len(parts) == 2 && parts[1] != "" {
kind := strings.ToLower(strings.TrimSpace(parts[0]))