ultisuite-backend/internal/orgpolicy/loader.go
R3D347HR4Y b90edf317c
Some checks failed
CI / Go tests (push) Has been cancelled
CI / Integration tests (push) Has been cancelled
CI / DB migrations (push) Has been cancelled
feat(scan): add VirusTotal upload antivirus
Admin-stored API key with env fallback; scan drive/mail/IMAP uploads.
Fail-open if VT down, 422 on malware; migration for virus_scan_status.
2026-06-07 22:05:27 +02:00

136 lines
2.6 KiB
Go

package orgpolicy
import (
"context"
"encoding/json"
"strings"
"sync"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/ultisuite/ulti-backend/internal/config"
)
const orgSettingsSingletonID = 1
const defaultMaxUploadMiB = 512
// FilePolicies holds runtime file upload policy from org settings.
type FilePolicies struct {
VirusScanEnabled bool
VirusTotalAPIKey string
MaxUploadBytes int64
}
type Loader struct {
db *pgxpool.Pool
cfg *config.Config
mu sync.Mutex
cached FilePolicies
cachedAt time.Time
ttl time.Duration
}
func NewLoader(db *pgxpool.Pool, cfg *config.Config) *Loader {
return &Loader{
db: db,
cfg: cfg,
ttl: 60 * time.Second,
}
}
func (l *Loader) FilePolicies(ctx context.Context) (FilePolicies, error) {
l.mu.Lock()
if !l.cachedAt.IsZero() && time.Since(l.cachedAt) < l.ttl {
out := l.cached
l.mu.Unlock()
return out, nil
}
l.mu.Unlock()
fp, err := l.loadFilePolicies(ctx)
if err != nil {
return FilePolicies{}, err
}
l.mu.Lock()
l.cached = fp
l.cachedAt = time.Now()
l.mu.Unlock()
return fp, nil
}
func (l *Loader) loadFilePolicies(ctx context.Context) (FilePolicies, error) {
var raw []byte
err := l.db.QueryRow(ctx, `
SELECT settings FROM org_settings WHERE id = $1
`, orgSettingsSingletonID).Scan(&raw)
if err != nil && err != pgx.ErrNoRows {
return FilePolicies{}, err
}
stored := map[string]any{}
if len(raw) > 0 {
if err := json.Unmarshal(raw, &stored); err != nil {
return FilePolicies{}, err
}
}
filePolicies, _ := stored["file_policies"].(map[string]any)
enabled := boolValue(filePolicies["virus_scan_enabled"])
apiKey := stringValue(filePolicies["virustotal_api_key"])
if strings.TrimSpace(apiKey) == "" && l.cfg != nil {
apiKey = strings.TrimSpace(l.cfg.VirusTotalAPIKey)
}
maxMiB := int64(defaultMaxUploadMiB)
switch v := filePolicies["max_upload_mib"].(type) {
case float64:
if v > 0 {
maxMiB = int64(v)
}
case int:
if v > 0 {
maxMiB = int64(v)
}
case int64:
if v > 0 {
maxMiB = v
}
}
return FilePolicies{
VirusScanEnabled: enabled,
VirusTotalAPIKey: apiKey,
MaxUploadBytes: maxMiB * 1024 * 1024,
}, nil
}
func (l *Loader) ScanEnabled(ctx context.Context) (bool, string, error) {
fp, err := l.FilePolicies(ctx)
if err != nil {
return false, "", err
}
if !fp.VirusScanEnabled || strings.TrimSpace(fp.VirusTotalAPIKey) == "" {
return false, "", nil
}
return true, fp.VirusTotalAPIKey, nil
}
func boolValue(v any) bool {
switch t := v.(type) {
case bool:
return t
default:
return false
}
}
func stringValue(v any) string {
s, _ := v.(string)
return s
}