Files-basierter RADIUS-Server (Clients + Users), managed analog DHCP/WireGuard.
- Migration 0042: radius_settings (singleton, node-lokal), radius_clients (secret_enc), radius_users (password_enc) — Secrets via secrets.Box verschlüsselt.
- internal/freeradius: Multi-File-Renderer (clients.conf + authorize) via Box.Open, Secret-Escaping (" \), Service default-off/an enabled gekoppelt. internal/services/radius + internal/handlers/radius.go: Settings + Client/User-CRUD, write-only Secret-Semantik, Validierung (IP/CIDR, name-charset), GET liefert secret_configured statt Secret.
- Firewall: udp 1812/1813 Auto-Rule bei enabled. Cluster: clients/users repliziert (hashSpec), radius_settings node-lokal.
- main.go + render.go + WithAllReloaders. Packaging: freeradius Dependency, setgid-Dir /etc/edgeguard/freeradius (Gruppe freeradius), Symlinks clients.conf+authorize, disable-on-install, sudoers.
- UI: RADIUS-Seite (Einstellungen + Clients + Benutzer) unter Sicherheit, Route/Nav/i18n de/en.
- Tests (guarded): Renderer-Inhalt + Secret-Escaping/Roundtrip + Masking. Scope v1: PAP/CHAP files-based (kein EAP/802.1X).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.2 KiB
Go
84 lines
2.2 KiB
Go
package freeradius
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
|
radiussvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/radius"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/secrets"
|
|
)
|
|
|
|
// Guarded integration test: set EG_FWTEST_DSN (sonst skip).
|
|
func TestRender_ClientsAndUsers(t *testing.T) {
|
|
dsn := os.Getenv("EG_FWTEST_DSN")
|
|
if dsn == "" {
|
|
t.Skip("set EG_FWTEST_DSN to run the freeradius renderer test")
|
|
}
|
|
ctx := context.Background()
|
|
var mErr error
|
|
for i := 0; i < 3; i++ {
|
|
if mErr = database.Migrate(ctx, dsn); mErr == nil {
|
|
break
|
|
}
|
|
time.Sleep(700 * time.Millisecond)
|
|
}
|
|
if mErr != nil {
|
|
t.Fatalf("migrate: %v", mErr)
|
|
}
|
|
pool, err := database.Open(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
for _, q := range []string{`DELETE FROM radius_clients`, `DELETE FROM radius_users`} {
|
|
if _, err := pool.Exec(ctx, q); err != nil {
|
|
t.Fatalf("clean: %v", err)
|
|
}
|
|
}
|
|
|
|
box := secrets.New(t.TempDir() + "/master_key")
|
|
repo := radiussvc.New(pool, box)
|
|
if _, err := repo.CreateClient(ctx, "testnas", "10.0.0.0/24", `s3c"ret\x`, true, "lab"); err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
if _, err := repo.CreateUser(ctx, "alice", "alicepw", true); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
|
|
g := New(pool, box)
|
|
|
|
clients, err := g.buildClients(ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("buildClients: %v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"client testnas {",
|
|
"ipaddr = 10.0.0.0/24",
|
|
`secret = "s3c\"ret\\x"`, // " und \ escaped → Secret-Roundtrip + Escaping
|
|
"shortname = testnas",
|
|
} {
|
|
if !strings.Contains(clients, want) {
|
|
t.Errorf("clients.conf missing %q\n----\n%s", want, clients)
|
|
}
|
|
}
|
|
|
|
authorize, err := g.buildAuthorize(ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("buildAuthorize: %v", err)
|
|
}
|
|
if !strings.Contains(authorize, `"alice" Cleartext-Password := "alicepw"`) {
|
|
t.Errorf("authorize missing alice entry\n----\n%s", authorize)
|
|
}
|
|
|
|
// Maskierte Preview enthält keine echten Secrets.
|
|
masked, _ := g.buildClients(ctx, true)
|
|
if strings.Contains(masked, "s3c") {
|
|
t.Errorf("masked preview leaked secret:\n%s", masked)
|
|
}
|
|
}
|