Bind Feishu identity to OpenVPN sessions and refine management UI

This commit is contained in:
opsx committed 2026-10-22 23:24:41 +08:00
1 parent 347799cb1e
commit 8fd8c0cd73
27 files changed
+1556 -1011

No files matched your search

@@ -2,6 +2,7 @@ package openvpn
import (
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
@@ -52,6 +53,19 @@ func TestOIDCConfigAndSecrets(t *testing.T) {
if stat, err := os.Stat(path); err != nil || stat.Mode().Perm() != 0o600 {
t.Fatalf("auth configuration must be 0600: %v %v", stat, err)
}
pagePath := filepath.Join(service.InstanceDir(tunnel), "auth-result.html")
page, err := os.ReadFile(pagePath)
if err != nil {
t.Fatal(err)
}
if stat, err := os.Stat(pagePath); err != nil || stat.Mode().Perm() != 0o600 {
t.Fatalf("auth result template must be 0600: %v %v", stat, err)
}
for _, want := range []string{"name=\"viewport\"", "请返回 VPN 客户端", "请在 VPN 客户端查看连接状态"} {
if !strings.Contains(string(page), want) {
t.Fatalf("auth result template missing %q", want)
}
}
passwordPath := filepath.Join(service.InstanceDir(tunnel), "management-password.txt")
if stat, err := os.Stat(passwordPath); err != nil || stat.Mode().Perm() != 0o600 {
t.Fatalf("management password must be 0600: %v %v", stat, err)
@@ -79,6 +93,21 @@ func TestOIDCConfigAndSecrets(t *testing.T) {
}
}
func TestBuiltInOIDCUsesStableSubjectAsOpenVPNUsername(t *testing.T) {
tunnel := &model.Tunnel{ID: 44, Protocol: "openvpn"}
config := map[string]interface{}{
"oidc_provider": "builtin", "oidc_issuer": "https://vpn.example.com/id",
}
generated := oidcConfig(tunnel, config)
if !strings.Contains(generated, `openvpn-username: "string(token.claims.sub)"`) {
t.Fatalf("built-in issuer must use verified subject as username: %s", generated)
}
config["oidc_provider"] = "external"
if generated := oidcConfig(tunnel, config); strings.Contains(generated, "openvpn-username:") {
t.Fatalf("external issuer username behavior must remain unchanged: %s", generated)
}
}
func TestOIDCRejectsUnsafeURLs(t *testing.T) {
base := map[string]interface{}{
"auth_mode": "oidc", "oidc_issuer": "https://idp.example.com/realms/a",
@@ -335,3 +364,85 @@ func TestSplitStatusFieldsSupportsOpenVPNStatusV3(t *testing.T) {
t.Fatalf("status-version 3 fields parsed incorrectly: %#v", fields)
}
}
func TestStatusUsernameDoesNotTreatUNDEFAsIdentity(t *testing.T) {
for _, tc := range []struct {
name, cn, username, want string
}{
{"password", "admin", "admin", "admin"},
{"authenticated username", "UNDEF", "tenant:ou_123", "tenant:ou_123"},
{"certificate", "client-cert", "UNDEF", "client-cert"},
{"webauth without username", "UNDEF", "UNDEF", ""},
{"webauth empty username", "UNDEF", "", ""},
{"webauth lowercase placeholder", "undef", "UnDeF", ""},
} {
t.Run(tc.name, func(t *testing.T) {
fields := splitStatusFields("CLIENT_LIST\t" + tc.cn + "\t203.0.113.1:1234\t10.8.0.2\t\t12\t34\t2026-10-22\t1234\t" + tc.username)
if got := statusUsername(fields); got != tc.want {
t.Fatalf("statusUsername() = %q, want %q", got, tc.want)
}
})
}
}
func TestWebAuthSessionIdentityAndTrafficTransition(t *testing.T) {
dir := t.TempDir()
t.Setenv("VPN_DATA_DIR", dir)
previousDB := store.DB
t.Cleanup(func() {
if store.DB != nil {
if sqlDB, err := store.DB.DB(); err == nil {
_ = sqlDB.Close()
}
}
store.DB = previousDB
})
if err := store.Init(dir); err != nil {
t.Fatal(err)
}
tunnel := &model.Tunnel{ID: 502, Name: "webauth-test", Protocol: "openvpn"}
if err := store.DB.Create(tunnel).Error; err != nil {
t.Fatal(err)
}
for _, username := range []string{"UNDEF", "tenant:ou_123"} {
if err := store.DB.Create(&model.VPNUser{TunnelID: tunnel.ID, Username: username, Enabled: true}).Error; err != nil {
t.Fatal(err)
}
}
instanceDir := service.InstanceDir(tunnel)
if err := os.MkdirAll(instanceDir, 0o700); err != nil {
t.Fatal(err)
}
writeStatus := func(username string, rx, tx int64) {
t.Helper()
line := fmt.Sprintf("CLIENT_LIST\tUNDEF\t203.0.113.1:1234\t10.8.0.2\t\t%d\t%d\t2026-10-22\t1234\t%s\n", rx, tx, username)
if err := os.WriteFile(filepath.Join(instanceDir, "openvpn-status.log"), []byte(line), 0o600); err != nil {
t.Fatal(err)
}
}
writeStatus("UNDEF", 150, 230)
svc := New()
if rx, tx := svc.collectTraffic(tunnel); rx != 150 || tx != 230 {
t.Fatalf("initial traffic = %d/%d", rx, tx)
}
var unknown model.VPNSession
if err := store.DB.Where("tunnel_id = ? AND status = ?", tunnel.ID, "active").First(&unknown).Error; err != nil || unknown.Username != "" || unknown.VPNUserID != 0 {
t.Fatalf("unknown peer must not be attributed: %+v, %v", unknown, err)
}
var placeholder model.VPNUser
store.DB.Where("tunnel_id = ? AND username = ?", tunnel.ID, "UNDEF").First(&placeholder)
if placeholder.RxBytes != 0 || placeholder.TxBytes != 0 {
t.Fatal("UNDEF placeholder received VPN user traffic")
}
if rx, tx := svc.collectTraffic(tunnel); rx != 0 || tx != 0 {
t.Fatalf("unchanged snapshot counted twice: %d/%d", rx, tx)
}
writeStatus("tenant:ou_123", 160, 260)
if rx, tx := svc.collectTraffic(tunnel); rx != 10 || tx != 30 {
t.Fatalf("authenticated snapshot delta = %d/%d, want 10/30", rx, tx)
}
var identified model.VPNSession
if err := store.DB.Where("tunnel_id = ? AND status = ?", tunnel.ID, "active").First(&identified).Error; err != nil || identified.Username != "tenant:ou_123" || identified.VPNUserID == 0 {
t.Fatalf("verified username should link its own user: %+v, %v", identified, err)
}
}