feat(admin): add security and activity management features
This commit is contained in:
217
lib/security/guard.go
Normal file
217
lib/security/guard.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
IPWhitelist string
|
||||
AdminIPWhitelist string
|
||||
LoginWindowSeconds int64
|
||||
LoginMaxAttempts int
|
||||
BanSeconds int64
|
||||
ScanWindowSeconds int64
|
||||
ScanMaxAttempts int
|
||||
UploadWindowSeconds int64
|
||||
UploadMaxRequests int
|
||||
UploadMaxBytes int64
|
||||
}
|
||||
|
||||
type Guard struct {
|
||||
mu sync.Mutex
|
||||
failedLogins map[string][]time.Time
|
||||
scanAttempts map[string][]time.Time
|
||||
uploadEvents map[string][]uploadEvent
|
||||
bannedUntil map[string]time.Time
|
||||
ipWhitelist map[string]bool
|
||||
adminWhitelist map[string]bool
|
||||
}
|
||||
|
||||
type uploadEvent struct {
|
||||
at time.Time
|
||||
bytes int64
|
||||
}
|
||||
|
||||
type BanEntry struct {
|
||||
IP string `json:"ip"`
|
||||
Until time.Time `json:"until"`
|
||||
}
|
||||
|
||||
func NewGuard() *Guard {
|
||||
return &Guard{
|
||||
failedLogins: map[string][]time.Time{},
|
||||
scanAttempts: map[string][]time.Time{},
|
||||
uploadEvents: map[string][]uploadEvent{},
|
||||
bannedUntil: map[string]time.Time{},
|
||||
ipWhitelist: map[string]bool{},
|
||||
adminWhitelist: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Guard) Reload(cfg Config) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.ipWhitelist = parseList(cfg.IPWhitelist)
|
||||
g.adminWhitelist = parseList(cfg.AdminIPWhitelist)
|
||||
}
|
||||
|
||||
func (g *Guard) IsWhitelisted(ip string) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.ipWhitelist[ip]
|
||||
}
|
||||
|
||||
func (g *Guard) IsAdminWhitelisted(ip string) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.adminWhitelist[ip] || g.ipWhitelist[ip]
|
||||
}
|
||||
|
||||
func (g *Guard) IsBanned(ip string) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
until, ok := g.bannedUntil[ip]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if time.Now().UTC().After(until) {
|
||||
delete(g.bannedUntil, ip)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Guard) Ban(ip string, seconds int64) {
|
||||
if seconds <= 0 || ip == "" {
|
||||
return
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.bannedUntil[ip] = time.Now().UTC().Add(time.Duration(seconds) * time.Second)
|
||||
}
|
||||
|
||||
func (g *Guard) BanUntil(ip string, until time.Time) {
|
||||
if ip == "" || until.IsZero() {
|
||||
return
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.bannedUntil[ip] = until.UTC()
|
||||
}
|
||||
|
||||
func (g *Guard) Unban(ip string) {
|
||||
if ip == "" {
|
||||
return
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
delete(g.bannedUntil, ip)
|
||||
}
|
||||
|
||||
func (g *Guard) BanList() []BanEntry {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
out := make([]BanEntry, 0, len(g.bannedUntil))
|
||||
for ip, until := range g.bannedUntil {
|
||||
if now.After(until) {
|
||||
delete(g.bannedUntil, ip)
|
||||
continue
|
||||
}
|
||||
out = append(out, BanEntry{IP: ip, Until: until})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].Until.Before(out[j].Until)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (g *Guard) RegisterFailedLogin(ip string, windowSeconds int64, maxAttempts int, banSeconds int64) (bool, int) {
|
||||
if ip == "" || maxAttempts <= 0 || windowSeconds <= 0 {
|
||||
return false, 0
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
cutoff := now.Add(-time.Duration(windowSeconds) * time.Second)
|
||||
attempts := pruneTimes(g.failedLogins[ip], cutoff)
|
||||
attempts = append(attempts, now)
|
||||
g.failedLogins[ip] = attempts
|
||||
if len(attempts) >= maxAttempts {
|
||||
g.bannedUntil[ip] = now.Add(time.Duration(banSeconds) * time.Second)
|
||||
return true, len(attempts)
|
||||
}
|
||||
return false, len(attempts)
|
||||
}
|
||||
|
||||
func (g *Guard) RegisterScanAttempt(ip string, windowSeconds int64, maxAttempts int, banSeconds int64) (bool, int) {
|
||||
if ip == "" || maxAttempts <= 0 || windowSeconds <= 0 {
|
||||
return false, 0
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
cutoff := now.Add(-time.Duration(windowSeconds) * time.Second)
|
||||
attempts := pruneTimes(g.scanAttempts[ip], cutoff)
|
||||
attempts = append(attempts, now)
|
||||
g.scanAttempts[ip] = attempts
|
||||
if len(attempts) >= maxAttempts {
|
||||
g.bannedUntil[ip] = now.Add(time.Duration(banSeconds) * time.Second)
|
||||
return true, len(attempts)
|
||||
}
|
||||
return false, len(attempts)
|
||||
}
|
||||
|
||||
func (g *Guard) AllowUpload(ip string, size int64, windowSeconds int64, maxRequests int, maxBytes int64) (bool, int, int64) {
|
||||
if ip == "" || windowSeconds <= 0 || maxRequests <= 0 {
|
||||
return true, 0, 0
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
cutoff := now.Add(-time.Duration(windowSeconds) * time.Second)
|
||||
events := g.uploadEvents[ip]
|
||||
kept := make([]uploadEvent, 0, len(events)+1)
|
||||
totalBytes := int64(0)
|
||||
for _, event := range events {
|
||||
if event.at.After(cutoff) {
|
||||
kept = append(kept, event)
|
||||
totalBytes += event.bytes
|
||||
}
|
||||
}
|
||||
nextCount := len(kept) + 1
|
||||
nextBytes := totalBytes + size
|
||||
if nextCount > maxRequests {
|
||||
return false, nextCount, nextBytes
|
||||
}
|
||||
if maxBytes > 0 && nextBytes > maxBytes {
|
||||
return false, nextCount, nextBytes
|
||||
}
|
||||
kept = append(kept, uploadEvent{at: now, bytes: size})
|
||||
g.uploadEvents[ip] = kept
|
||||
return true, nextCount, nextBytes
|
||||
}
|
||||
|
||||
func pruneTimes(values []time.Time, cutoff time.Time) []time.Time {
|
||||
kept := make([]time.Time, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value.After(cutoff) {
|
||||
kept = append(kept, value)
|
||||
}
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
||||
func parseList(raw string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, chunk := range strings.Split(raw, ",") {
|
||||
value := strings.TrimSpace(chunk)
|
||||
if value != "" {
|
||||
out[value] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user