Smail/server/config/runtime_validator_test.go
smail 29cfebd620
Some checks are pending
Security Checks / security (push) Waiting to run
完善配置安全校验与数据库迁移处理
2026-08-30 17:45:52 +08:00

153 lines
6.3 KiB
Go

package config
import (
"encoding/json"
"errors"
"git.pcux.cn/xuans/Smail/plugins/catalog"
"testing"
)
func validRuntimeConfig() RuntimeConfig {
root := testRootConfig()
root.SetupState = SetupStateReady
root.Domains = DomainConfig{Primary: "mail.example.com", Accepted: []string{"mail.example.com", "example.com"}, Web: "mail.example.com"}
root.HTTP = HTTPConfig{Port: 80, HTTPSPort: 443, HTTPSEnabled: true}
root.Sections = validSectionReferences(root.Generation)
meta := SectionMeta{Schema: CurrentSectionSchema, Generation: root.Generation}
return RuntimeConfig{
Generation: root.Generation,
SetupState: root.SetupState,
Root: root,
SMTP: SMTPSectionConfig{Meta: meta, Relay: SMTPConfig{}},
Email: EmailSectionConfig{Meta: meta, Policy: EmailConfig{
SMTPPort: 25,
IMAPPort: 143,
POP3Port: 110,
}},
SSL: SSLSectionConfig{Meta: meta, TLS: root.SSL},
Plugins: PluginsSectionConfig{Meta: meta, Plugins: map[string]PluginInstance{
"telegram_push": {
Enabled: true,
Executable: currentPluginExecutable("telegram_push"),
Permissions: PluginPermissions{
Capabilities: []string{"network"},
Network: []PluginNetworkTarget{{Scheme: "https", Host: "api.telegram.org", Port: 443}},
},
Settings: json.RawMessage(`{"displayName":"Telegram"}`),
},
}},
}
}
func currentPluginExecutable(logicalID string) string {
executable, err := catalog.CurrentExecutableName(logicalID)
if err != nil {
panic(err)
}
return executable
}
func TestValidateRuntimeConfig(t *testing.T) {
runtime := validRuntimeConfig()
runtime.Root.Domains.AccessHosts = []string{"192.0.2.10", "reset.example.com"}
if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true, Production: true}); err != nil {
t.Fatal(err)
}
}
func TestValidateRuntimeConfigRejectsCrossSectionViolations(t *testing.T) {
tests := []struct {
name string
mutate func(*RuntimeConfig, *RuntimeValidationOptions)
want error
}{
{name: "port_conflict", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) { runtime.Email.Policy.IMAPPort = 443 }, want: ErrPortConflict},
{name: "primary_domain_missing", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
runtime.Root.Domains.Accepted = []string{"example.com"}
}, want: ErrInvalidDomainConfig},
{name: "generation_mismatch", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) { runtime.Email.Meta.Generation++ }, want: ErrSectionGenerationMismatch},
{name: "missing_super_admin", mutate: func(_ *RuntimeConfig, options *RuntimeValidationOptions) { options.HasSuperAdmin = false }, want: ErrInvalidRuntimeConfig},
{name: "network_without_capability", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
plugin := runtime.Plugins.Plugins["telegram_push"]
plugin.Permissions.Capabilities = nil
runtime.Plugins.Plugins["telegram_push"] = plugin
}, want: ErrPluginCapability},
{name: "insecure_plugin_scheme", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
plugin := runtime.Plugins.Plugins["telegram_push"]
plugin.Permissions.Network[0].Scheme = "http"
runtime.Plugins.Plugins["telegram_push"] = plugin
}, want: ErrPluginCapability},
{name: "plugin_secret_in_settings", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
plugin := runtime.Plugins.Plugins["telegram_push"]
plugin.Settings = json.RawMessage(`{"botToken":"secret"}`)
runtime.Plugins.Plugins["telegram_push"] = plugin
}, want: ErrInvalidPlugin},
{name: "plugin_executable_mismatch", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
plugin := runtime.Plugins.Plugins["telegram_push"]
plugin.Executable = "untrusted_plugin.exe"
runtime.Plugins.Plugins["telegram_push"] = plugin
}, want: ErrInvalidPlugin},
{name: "plugin_capability_escalation", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
plugin := runtime.Plugins.Plugins["telegram_push"]
plugin.Permissions.Capabilities = append(plugin.Permissions.Capabilities, "filesystem")
runtime.Plugins.Plugins["telegram_push"] = plugin
}, want: ErrPluginCapability},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
runtime := validRuntimeConfig()
options := RuntimeValidationOptions{HasSuperAdmin: true, Production: true}
test.mutate(&runtime, &options)
if err := ValidateRuntimeConfig(runtime, options); !errors.Is(err, test.want) {
t.Fatalf("error = %v, want %v", err, test.want)
}
})
}
}
func TestProductionDegradedRuntimeRequiresHTTPS(t *testing.T) {
runtime := validRuntimeConfig()
runtime.SetupState = SetupStateDegraded
runtime.Root.SetupState = SetupStateDegraded
runtime.Root.HTTP.HTTPSEnabled = false
runtime.Root.HTTP.HTTPSPort = 0
if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true, Production: true}); !errors.Is(err, ErrInvalidRuntimeConfig) {
t.Fatalf("error = %v", err)
}
if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true, Production: false}); err != nil {
t.Fatal(err)
}
}
func TestValidateHTTPProxyMode(t *testing.T) {
runtime := validRuntimeConfig()
runtime.Root.HTTP = HTTPConfig{
Port: 15184,
TLSMode: HTTPSTLSModeProxy,
TrustedProxyCIDRs: []string{"127.0.0.1/32", "::1/128"},
}
if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true, Production: true}); err != nil {
t.Fatalf("proxy mode was rejected: %v", err)
}
invalid := runtime
invalid.Root.HTTP.HTTPSPort = 443
if err := ValidateRuntimeConfig(invalid, RuntimeValidationOptions{HasSuperAdmin: true, Production: true}); !errors.Is(err, ErrInvalidHTTP) {
t.Fatalf("proxy mode with HTTPS port returned %v, want %v", err, ErrInvalidHTTP)
}
}
func TestValidateHTTPRejectsInvalidProxySettings(t *testing.T) {
cases := []HTTPConfig{
{Port: 15184, TLSMode: "unsupported"},
{Port: 15184, TLSMode: HTTPSTLSModeProxy, HTTPSEnabled: true},
{Port: 15184, TLSMode: HTTPSTLSModeProxy, TrustedProxyCIDRs: []string{"0.0.0.0"}},
{Port: 15184, TLSMode: HTTPSTLSModeProxy, TrustedProxyCIDRs: []string{" 127.0.0.1/32"}},
}
for index, candidate := range cases {
if err := validateHTTP(candidate); !errors.Is(err, ErrInvalidHTTP) {
t.Fatalf("case %d returned %v, want %v", index, err, ErrInvalidHTTP)
}
}
}