mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-21 16:01:28 +02:00
Compare commits
8 Commits
fix/routes
...
windows-sl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25fe7d13d9 | ||
|
|
c533dead28 | ||
|
|
21fc5b81f6 | ||
|
|
67af1b2ef2 | ||
|
|
9906b9b1a1 | ||
|
|
877e889250 | ||
|
|
d15830a2d0 | ||
|
|
141f3d0390 |
@@ -292,18 +292,16 @@ func (s *serviceViaListener) generateFreePort() (uint16, error) {
|
||||
return customPort, nil
|
||||
}
|
||||
|
||||
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort("0.0.0.0:0"))
|
||||
probeListener, err := net.ListenUDP("udp", udpAddr)
|
||||
probeListener, err := net.ListenUDP("udp4", &net.UDPAddr{})
|
||||
if err != nil {
|
||||
log.Debugf("failed to bind random port for DNS: %s", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
addrPort := netip.MustParseAddrPort(probeListener.LocalAddr().String()) // might panic if address is incorrect
|
||||
err = probeListener.Close()
|
||||
if err != nil {
|
||||
port := uint16(probeListener.LocalAddr().(*net.UDPAddr).Port)
|
||||
if err = probeListener.Close(); err != nil {
|
||||
log.Debugf("failed to free up DNS port: %s", err)
|
||||
return 0, err
|
||||
}
|
||||
return addrPort.Port(), nil
|
||||
return port, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !darwin || ios
|
||||
//go:build (!darwin && !windows) || ios
|
||||
|
||||
package sleep
|
||||
|
||||
|
||||
255
client/internal/sleep/detector_windows.go
Normal file
255
client/internal/sleep/detector_windows.go
Normal file
@@ -0,0 +1,255 @@
|
||||
//go:build windows
|
||||
|
||||
package sleep
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Power broadcast event types delivered to the notification callback.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/power/power-management-events
|
||||
const (
|
||||
pbtAPMSuspend uintptr = 0x0004
|
||||
pbtAPMResumeSuspend uintptr = 0x0007
|
||||
pbtAPMResumeAutomatic uintptr = 0x0012
|
||||
deviceNotifyCallback uintptr = 0x00000002
|
||||
)
|
||||
|
||||
var (
|
||||
// The callback flavor of these functions is exported by user32.dll, not
|
||||
// powrprof.dll (which exports the differently-shaped Power* variants).
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registersuspendresumenotification
|
||||
registerSuspendResumeNotification = user32.NewProc("RegisterSuspendResumeNotification")
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregistersuspendresumenotification
|
||||
unregisterSuspendResumeNotification = user32.NewProc("UnregisterSuspendResumeNotification")
|
||||
|
||||
libInitOnce sync.Once
|
||||
libInitErr error
|
||||
|
||||
// callbackThunk is the single C-callable trampoline registered with the OS.
|
||||
// windows.NewCallback slots are a finite, non-reclaimable resource, so a
|
||||
// single thunk dispatches to the Detector identified by the callback Context.
|
||||
callbackThunk uintptr
|
||||
|
||||
// registry maps the Context value handed to the OS callback back to the
|
||||
// Detector that registered it, mirroring darwin's serviceRegistry.
|
||||
registry = make(map[int]*Detector)
|
||||
registryMu sync.Mutex
|
||||
nextHandle int
|
||||
|
||||
// lifecycleMu serializes Register/Deregister so concurrent lifecycle calls
|
||||
// can't race on the shared registry or the OS registration handle.
|
||||
lifecycleMu sync.Mutex
|
||||
)
|
||||
|
||||
// deviceNotifySubscribeParameters is DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-device_notify_subscribe_parameters
|
||||
type deviceNotifySubscribeParameters struct {
|
||||
Callback uintptr
|
||||
Context uintptr
|
||||
}
|
||||
|
||||
// Detector delivers sleep and wake events to a registered callback.
|
||||
type Detector struct {
|
||||
callback func(event EventType)
|
||||
done chan struct{}
|
||||
|
||||
// handle keys this detector in the package registry and is passed to the OS
|
||||
// as the callback Context. Zero means the detector is not registered.
|
||||
handle int
|
||||
// hPowerNotify is the HPOWERNOTIFY returned by RegisterSuspendResumeNotification.
|
||||
hPowerNotify uintptr
|
||||
// params is kept alive for the lifetime of the registration so the OS never
|
||||
// dereferences freed memory.
|
||||
params deviceNotifySubscribeParameters
|
||||
}
|
||||
|
||||
// NewDetector resolves powrprof.dll symbols and returns a Detector.
|
||||
func NewDetector() (*Detector, error) {
|
||||
if err := initLibs(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Detector{}, nil
|
||||
}
|
||||
|
||||
func initLibs() error {
|
||||
libInitOnce.Do(func() {
|
||||
if err := registerSuspendResumeNotification.Find(); err != nil {
|
||||
libInitErr = fmt.Errorf("resolve RegisterSuspendResumeNotification: %w", err)
|
||||
return
|
||||
}
|
||||
if err := unregisterSuspendResumeNotification.Find(); err != nil {
|
||||
libInitErr = fmt.Errorf("resolve UnregisterSuspendResumeNotification: %w", err)
|
||||
return
|
||||
}
|
||||
callbackThunk = windows.NewCallback(powerCallback)
|
||||
})
|
||||
return libInitErr
|
||||
}
|
||||
|
||||
// Register installs callback for power events and subscribes to suspend/resume
|
||||
// notifications via powrprof.dll.
|
||||
func (d *Detector) Register(callback func(event EventType)) error {
|
||||
lifecycleMu.Lock()
|
||||
defer lifecycleMu.Unlock()
|
||||
|
||||
registryMu.Lock()
|
||||
if d.handle != 0 {
|
||||
registryMu.Unlock()
|
||||
return fmt.Errorf("detector service already registered")
|
||||
}
|
||||
d.callback = callback
|
||||
d.done = make(chan struct{})
|
||||
nextHandle++
|
||||
handle := nextHandle
|
||||
d.handle = handle
|
||||
registry[handle] = d
|
||||
registryMu.Unlock()
|
||||
|
||||
d.params = deviceNotifySubscribeParameters{
|
||||
Callback: callbackThunk,
|
||||
Context: uintptr(handle),
|
||||
}
|
||||
|
||||
ret, _, callErr := registerSuspendResumeNotification.Call(
|
||||
uintptr(unsafe.Pointer(&d.params)),
|
||||
deviceNotifyCallback,
|
||||
)
|
||||
if ret == 0 {
|
||||
registryMu.Lock()
|
||||
delete(registry, handle)
|
||||
close(d.done)
|
||||
d.done = nil
|
||||
d.handle = 0
|
||||
registryMu.Unlock()
|
||||
return fmt.Errorf("RegisterSuspendResumeNotification failed: %w", callErr)
|
||||
}
|
||||
d.hPowerNotify = ret
|
||||
|
||||
log.Info("sleep detection service started on Windows")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deregister unsubscribes from power notifications and removes the detector.
|
||||
func (d *Detector) Deregister() error {
|
||||
lifecycleMu.Lock()
|
||||
defer lifecycleMu.Unlock()
|
||||
|
||||
registryMu.Lock()
|
||||
if d.handle == 0 {
|
||||
registryMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
handle := d.handle
|
||||
hPowerNotify := d.hPowerNotify
|
||||
done := d.done
|
||||
registryMu.Unlock()
|
||||
|
||||
log.Info("sleep detection service stopping (deregister)")
|
||||
|
||||
// Unregister the OS subscription first. If it fails, leave handle and
|
||||
// hPowerNotify intact so a later call can retry the cleanup.
|
||||
if hPowerNotify != 0 {
|
||||
ret, _, callErr := unregisterSuspendResumeNotification.Call(hPowerNotify)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("UnregisterSuspendResumeNotification failed: %w", callErr)
|
||||
}
|
||||
}
|
||||
|
||||
registryMu.Lock()
|
||||
close(done)
|
||||
delete(registry, handle)
|
||||
d.handle = 0
|
||||
d.hPowerNotify = 0
|
||||
registryMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Detector) triggerCallback(event EventType, cb func(event EventType), done <-chan struct{}) {
|
||||
if cb == nil || done == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
doneChan := make(chan struct{})
|
||||
// The OS invokes this callback synchronously on the suspend path, so run the
|
||||
// teardown inline with a bounded budget (mirroring the macOS detector) so
|
||||
// Down completes before the machine suspends without blocking indefinitely.
|
||||
timeout := time.NewTimer(20 * time.Second)
|
||||
defer timeout.Stop()
|
||||
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("panic in sleep callback: %v", r)
|
||||
}
|
||||
}()
|
||||
log.Info("sleep detection event fired")
|
||||
cb(event)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-doneChan:
|
||||
case <-done:
|
||||
case <-timeout.C:
|
||||
log.Warn("sleep callback timed out")
|
||||
}
|
||||
}
|
||||
|
||||
// powerCallback is the DEVICE_NOTIFY_CALLBACK_ROUTINE trampoline, invoked by the
|
||||
// OS on a system thread. A Go panic crossing the syscall boundary has undefined
|
||||
// behavior, so contain it here. It must return ERROR_SUCCESS (0).
|
||||
func powerCallback(context uintptr, msgType uintptr, setting uintptr) uintptr {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("panic in sleep powerCallback: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var event EventType
|
||||
switch msgType {
|
||||
case pbtAPMSuspend:
|
||||
event = EventTypeSleep
|
||||
case pbtAPMResumeAutomatic, pbtAPMResumeSuspend:
|
||||
event = EventTypeWakeUp
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
||||
dispatchEvent(int(context), event)
|
||||
return 0
|
||||
}
|
||||
|
||||
func dispatchEvent(handle int, event EventType) {
|
||||
registryMu.Lock()
|
||||
d := registry[handle]
|
||||
var (
|
||||
cb func(event EventType)
|
||||
done <-chan struct{}
|
||||
)
|
||||
if d != nil {
|
||||
cb = d.callback
|
||||
done = d.done
|
||||
}
|
||||
registryMu.Unlock()
|
||||
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
d.triggerCallback(event, cb, done)
|
||||
}
|
||||
83
client/internal/sleep/detector_windows_test.go
Normal file
83
client/internal/sleep/detector_windows_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
//go:build windows
|
||||
|
||||
package sleep
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// registerFake inserts a detector into the registry without touching the OS,
|
||||
// so dispatch routing can be exercised in isolation. It returns the handle and
|
||||
// a cleanup that removes the entry.
|
||||
func registerFake(t *testing.T, cb func(EventType)) (int, func()) {
|
||||
t.Helper()
|
||||
|
||||
registryMu.Lock()
|
||||
nextHandle++
|
||||
handle := nextHandle
|
||||
d := &Detector{callback: cb, done: make(chan struct{}), handle: handle}
|
||||
registry[handle] = d
|
||||
registryMu.Unlock()
|
||||
|
||||
return handle, func() {
|
||||
registryMu.Lock()
|
||||
delete(registry, handle)
|
||||
registryMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPowerCallback_MapsMessageTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
msgType uintptr
|
||||
want EventType
|
||||
fires bool
|
||||
}{
|
||||
{"suspend", pbtAPMSuspend, EventTypeSleep, true},
|
||||
{"resume automatic", pbtAPMResumeAutomatic, EventTypeWakeUp, true},
|
||||
{"resume suspend", pbtAPMResumeSuspend, EventTypeWakeUp, true},
|
||||
{"unknown", 0x9999, EventTypeUnknown, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := make(chan EventType, 1)
|
||||
handle, cleanup := registerFake(t, func(e EventType) { got <- e })
|
||||
defer cleanup()
|
||||
|
||||
ret := powerCallback(uintptr(handle), tt.msgType, 0)
|
||||
require.Equal(t, uintptr(0), ret, "callback must return ERROR_SUCCESS")
|
||||
|
||||
if !tt.fires {
|
||||
assert.Empty(t, got, "no event should fire for unhandled message type")
|
||||
return
|
||||
}
|
||||
select {
|
||||
case e := <-got:
|
||||
assert.Equal(t, tt.want, e, "mapped event type should match")
|
||||
default:
|
||||
t.Fatal("expected callback to fire")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchEvent_UnknownHandleNoPanic(t *testing.T) {
|
||||
require.NotPanics(t, func() {
|
||||
dispatchEvent(-1, EventTypeSleep)
|
||||
}, "dispatch for an unregistered handle must be a no-op")
|
||||
}
|
||||
|
||||
func TestTriggerCallback_SkipsAfterDone(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
close(done)
|
||||
|
||||
fired := false
|
||||
d := &Detector{}
|
||||
d.triggerCallback(EventTypeSleep, func(EventType) { fired = true }, done)
|
||||
|
||||
assert.False(t, fired, "callback must not run once the detector is done")
|
||||
}
|
||||
@@ -44,10 +44,25 @@ type Auth struct {
|
||||
// NewAuth instantiate Auth struct and validate the management URL
|
||||
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
|
||||
inputCfg := profilemanager.ConfigInput{
|
||||
ConfigPath: cfgPath,
|
||||
ManagementURL: mgmURL,
|
||||
}
|
||||
|
||||
cfg, err := profilemanager.CreateInMemoryConfig(inputCfg)
|
||||
// Load the existing config when a config file is already present so an
|
||||
// interactive re-login reuses the peer's persisted WireGuard private key
|
||||
// (and thus its identity) instead of generating a fresh one. Generating a
|
||||
// new key registers a brand-new peer on the management server on every
|
||||
// re-auth (named after the fallback hostname). Only fall back to a fresh
|
||||
// in-memory config for the first-time login when no config file exists yet.
|
||||
// DirectUpdateOrCreateConfig uses non-atomic writes so it also works inside
|
||||
// the tvOS App Group sandbox where atomic temp-file+rename is blocked.
|
||||
var cfg *profilemanager.Config
|
||||
var err error
|
||||
if cfgPath != "" {
|
||||
cfg, err = profilemanager.DirectUpdateOrCreateConfig(inputCfg)
|
||||
} else {
|
||||
cfg, err = profilemanager.CreateInMemoryConfig(inputCfg)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -335,7 +335,7 @@ replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801
|
||||
|
||||
replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51
|
||||
|
||||
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.1-0.20260512110716-8d70ad8647c1
|
||||
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.1-0.20260716205454-a163de3129e5
|
||||
|
||||
replace github.com/dexidp/dex/api/v2 => github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1
|
||||
|
||||
|
||||
4
go.sum
4
go.sum
@@ -476,8 +476,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/netbirdio/dex v0.244.1-0.20260512110716-8d70ad8647c1 h1:4TaYr9O4xX0D2kszeOLclTiCbA3eHq3xWV+9ILJbIYs=
|
||||
github.com/netbirdio/dex v0.244.1-0.20260512110716-8d70ad8647c1/go.mod h1:IHH+H8vK2GfqtIt5u/5OdPh18yk0oDHuj2vz5+Goetg=
|
||||
github.com/netbirdio/dex v0.244.1-0.20260716205454-a163de3129e5 h1:3PwQv8aR46qN2u16+Dv6udnH3sbVKX5KrGwF35CKSI0=
|
||||
github.com/netbirdio/dex v0.244.1-0.20260716205454-a163de3129e5/go.mod h1:IHH+H8vK2GfqtIt5u/5OdPh18yk0oDHuj2vz5+Goetg=
|
||||
github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1 h1:neE7z+FPUkldl3faK/Jt+hJK2L+1XfQ1W33TQhU9m88=
|
||||
github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1/go.mod h1:awuTyT29CYALpEyET0S307EgNlPWrc7fFKRAyhsO45M=
|
||||
github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6Aus=
|
||||
|
||||
@@ -613,6 +613,10 @@ func (c *YAMLConfig) ToServerConfig(stor storage.Storage, logger *slog.Logger) s
|
||||
cfg.SupportedResponseTypes = c.OAuth2.ResponseTypes
|
||||
}
|
||||
|
||||
if len(c.OAuth2.GrantTypes) > 0 {
|
||||
cfg.AllowedGrantTypes = c.OAuth2.GrantTypes
|
||||
}
|
||||
|
||||
// Apply expiry settings
|
||||
if c.Expiry.IDTokens != "" {
|
||||
if d, err := parseDuration(c.Expiry.IDTokens); err == nil {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/dexidp/dex/server/signer"
|
||||
"github.com/dexidp/dex/storage"
|
||||
"github.com/dexidp/dex/storage/sql"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
@@ -595,3 +595,90 @@ enablePasswordDB: true
|
||||
assert.True(t, cfg.ContinueOnConnectorFailure,
|
||||
"buildDexConfig must set ContinueOnConnectorFailure to true so management starts even if an external IdP is down")
|
||||
}
|
||||
|
||||
func TestToServerConfig_WiresGrantTypes(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "dex-grants-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
stor := openTestStorage(t, tmpDir)
|
||||
defer stor.Close()
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
|
||||
grants := []string{"authorization_code", "refresh_token"}
|
||||
cfg := &YAMLConfig{Issuer: "http://localhost:5599/oauth2", OAuth2: OAuth2{GrantTypes: grants}}
|
||||
assert.Equal(t, grants, cfg.ToServerConfig(stor, logger).AllowedGrantTypes)
|
||||
|
||||
empty := &YAMLConfig{Issuer: "http://localhost:5599/oauth2"}
|
||||
assert.Empty(t, empty.ToServerConfig(stor, logger).AllowedGrantTypes)
|
||||
}
|
||||
|
||||
func newDeviceGuardProvider(t *testing.T, grantTypesYAML string) *Provider {
|
||||
t.Helper()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "dex-devguard-*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(tmpDir) })
|
||||
|
||||
yamlContent := `
|
||||
issuer: http://localhost:5599/oauth2
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: ` + filepath.Join(tmpDir, "dex.db") + `
|
||||
web:
|
||||
http: 127.0.0.1:5599
|
||||
enablePasswordDB: true
|
||||
` + grantTypesYAML
|
||||
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(configPath, []byte(yamlContent), 0644))
|
||||
|
||||
yamlConfig, err := LoadConfig(configPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider, err := NewProviderFromYAML(context.Background(), yamlConfig)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = provider.Stop(context.Background()) })
|
||||
return provider
|
||||
}
|
||||
|
||||
func TestHandler_BlocksDeviceEndpointsWhenDeviceGrantDisabled(t *testing.T) {
|
||||
provider := newDeviceGuardProvider(t, `
|
||||
oauth2:
|
||||
grantTypes:
|
||||
- authorization_code
|
||||
- refresh_token
|
||||
`)
|
||||
|
||||
devicePaths := []string{
|
||||
"/oauth2/device",
|
||||
"/oauth2/device/code",
|
||||
"/oauth2/device/token",
|
||||
"/oauth2/device/auth/verify_code",
|
||||
"/oauth2/device/callback",
|
||||
}
|
||||
for _, path := range devicePaths {
|
||||
for _, method := range []string{http.MethodGet, http.MethodPost} {
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
provider.Handler().ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code, "%s %s must be blocked", method, path)
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/oauth2/.well-known/openid-configuration", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
provider.Handler().ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestHandler_AllowsDeviceEndpointsWhenGrantsDefault(t *testing.T) {
|
||||
provider := newDeviceGuardProvider(t, "")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/oauth2/device/code", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
provider.Handler().ServeHTTP(rec, req)
|
||||
assert.NotEqual(t, http.StatusNotFound, rec.Code)
|
||||
}
|
||||
|
||||
@@ -4269,7 +4269,7 @@ func TestDefaultAccountManager_UpdateAccountSettings_NetworkRangePreserved(t *te
|
||||
}
|
||||
|
||||
// Sanity: an actually different range still triggers reallocation.
|
||||
newRange := netip.MustParsePrefix("100.99.0.0/16")
|
||||
newRange := netip.MustParsePrefix("100.60.0.0/16")
|
||||
_, err = manager.UpdateAccountSettings(ctx, account.Id, userID, &types.Settings{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: types.DefaultPeerLoginExpiration,
|
||||
|
||||
@@ -76,6 +76,9 @@ type EmbeddedIdPConfig struct {
|
||||
DashboardPostLogoutRedirectURIs []string
|
||||
// StaticConnectors are additional connectors to seed during initialization
|
||||
StaticConnectors []dex.Connector
|
||||
// GrantTypes restricts allowed OAuth2 grants; empty means all (Dex default). Omit the
|
||||
// device_code grant to disable the device flow; keep authorization_code and refresh_token.
|
||||
GrantTypes []string
|
||||
}
|
||||
|
||||
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
|
||||
@@ -175,6 +178,7 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
},
|
||||
OAuth2: dex.OAuth2{
|
||||
SkipApprovalScreen: true,
|
||||
GrantTypes: c.GrantTypes,
|
||||
},
|
||||
Frontend: dex.Frontend{
|
||||
Issuer: "NetBird",
|
||||
|
||||
@@ -1606,7 +1606,8 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
settings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,
|
||||
settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled,
|
||||
settings_local_mfa_enabled, settings_metrics_push_enabled, settings_agent_network_only,
|
||||
settings_dashboard_features,
|
||||
settings_dashboard_features, settings_auto_update_version, settings_auto_update_always,
|
||||
settings_peer_expose_enabled, settings_peer_expose_groups,
|
||||
-- Embedded ExtraSettings
|
||||
settings_extra_peer_approval_enabled, settings_extra_user_approval_required,
|
||||
settings_extra_integrated_validator, settings_extra_integrated_validator_groups
|
||||
@@ -1632,6 +1633,10 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
sMetricsPushEnabled sql.NullBool
|
||||
sAgentNetworkOnly sql.NullBool
|
||||
sDashboardFeatures sql.NullString
|
||||
autoUpdateVersion sql.NullString
|
||||
autoUpdateAlways sql.NullBool
|
||||
peerExposeEnabled sql.NullBool
|
||||
peerExposeGroups sql.NullString
|
||||
sExtraPeerApprovalEnabled sql.NullBool
|
||||
sExtraUserApprovalRequired sql.NullBool
|
||||
sExtraIntegratedValidator sql.NullString
|
||||
@@ -1655,7 +1660,8 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
|
||||
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
|
||||
&sLocalMFAEnabled, &sMetricsPushEnabled, &sAgentNetworkOnly,
|
||||
&sDashboardFeatures,
|
||||
&sDashboardFeatures, &autoUpdateVersion, &autoUpdateAlways,
|
||||
&peerExposeEnabled, &peerExposeGroups,
|
||||
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
|
||||
&sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups,
|
||||
)
|
||||
@@ -1747,6 +1753,18 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
if sIPv6EnabledGroups.Valid {
|
||||
_ = json.Unmarshal([]byte(sIPv6EnabledGroups.String), &account.Settings.IPv6EnabledGroups)
|
||||
}
|
||||
if autoUpdateAlways.Valid {
|
||||
account.Settings.AutoUpdateAlways = autoUpdateAlways.Bool
|
||||
}
|
||||
if autoUpdateVersion.Valid {
|
||||
account.Settings.AutoUpdateVersion = autoUpdateVersion.String
|
||||
}
|
||||
if peerExposeEnabled.Valid {
|
||||
account.Settings.PeerExposeEnabled = peerExposeEnabled.Bool
|
||||
}
|
||||
if peerExposeGroups.Valid {
|
||||
_ = json.Unmarshal([]byte(peerExposeGroups.String), &account.Settings.PeerExposeGroups)
|
||||
}
|
||||
|
||||
if sExtraPeerApprovalEnabled.Valid {
|
||||
account.Settings.Extra.PeerApprovalEnabled = sExtraPeerApprovalEnabled.Bool
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
"github.com/netbirdio/netbird/shared/testing_helpers"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
@@ -296,6 +298,53 @@ func Test_SaveAccount(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_AccountSettings_SaveAndRetrieve(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("The SQLite store is not properly supported by Windows yet")
|
||||
}
|
||||
|
||||
populateFields := testing_helpers.NewPopulateFields().WithCustomFieldSetter(
|
||||
reflect.PointerTo(reflect.TypeOf(types.ExtraSettings{})), func(this *testing_helpers.PopulateFields, field reflect.Value) (int, error) {
|
||||
es := types.ExtraSettings{}
|
||||
reflectedEs := reflect.ValueOf(&es).Elem()
|
||||
n, err := this.PopulateAll(reflectedEs)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
field.Set(reflectedEs.Addr())
|
||||
return n, nil
|
||||
}).WithCustomFieldSetter(
|
||||
reflect.PointerTo(reflect.TypeOf(types.DashboardFeatures{})), func(this *testing_helpers.PopulateFields, field reflect.Value) (int, error) {
|
||||
t := true
|
||||
df := types.DashboardFeatures{AgentNetwork: &t}
|
||||
reflectedDf := reflect.ValueOf(&df).Elem()
|
||||
field.Set(reflectedDf.Addr())
|
||||
return 1, nil
|
||||
}).WithSkippedTag("gorm", "-")
|
||||
|
||||
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
|
||||
account := newAccountWithId(context.Background(), "account_id", "testuser", "")
|
||||
setupKey, _ := types.GenerateDefaultSetupKey()
|
||||
account.SetupKeys[setupKey.Key] = setupKey
|
||||
|
||||
settings := types.Settings{}
|
||||
numOfExportedFields, err := populateFields.PopulateAll(reflect.ValueOf(&settings).Elem())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 27, numOfExportedFields)
|
||||
account.Settings = &settings
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
assert.NoError(t, err)
|
||||
|
||||
accountFromDb, err := store.GetAccount(context.Background(), account.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, accountFromDb)
|
||||
assert.NotNil(t, accountFromDb.Settings)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(&settings, accountFromDb.Settings), "created settings and settings retrieved from the db should match")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlite_DeleteAccount(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("The SQLite store is not properly supported by Windows yet")
|
||||
|
||||
101
shared/testing_helpers/populate_fields.go
Normal file
101
shared/testing_helpers/populate_fields.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package testing_helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type PopulateFields struct {
|
||||
CustomFieldSetters map[reflect.Type]func(this *PopulateFields, field reflect.Value) (int, error)
|
||||
TagsToSkip map[string]string
|
||||
}
|
||||
|
||||
func NewPopulateFields() *PopulateFields {
|
||||
return &PopulateFields{CustomFieldSetters: defaultCustomFieldSetters(), TagsToSkip: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (p *PopulateFields) WithCustomFieldSetter(t reflect.Type, f func(this *PopulateFields, field reflect.Value) (int, error)) *PopulateFields {
|
||||
p.CustomFieldSetters[t] = f
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PopulateFields) WithSkippedTag(tag, value string) *PopulateFields {
|
||||
p.TagsToSkip[tag] = value
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PopulateFields) PopulateAll(v reflect.Value) (int, error) {
|
||||
typ := v.Type()
|
||||
totalExportedFields := 0
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
f := typ.Field(i)
|
||||
if f.PkgPath != "" { // unexported
|
||||
continue
|
||||
}
|
||||
|
||||
if p.skippedTagPresent(f.Tag) {
|
||||
continue
|
||||
}
|
||||
|
||||
numOfExportedFields, err := p.setNonZero(v.Field(i))
|
||||
totalExportedFields += numOfExportedFields
|
||||
if err != nil {
|
||||
return totalExportedFields, err
|
||||
}
|
||||
}
|
||||
return totalExportedFields, nil
|
||||
}
|
||||
|
||||
// setNonZero assigns a deterministic non-zero value to a field based on its kind,
|
||||
// recursing into nested structs and populating one element of slice fields.
|
||||
func (p *PopulateFields) setNonZero(field reflect.Value) (int, error) {
|
||||
if f, ok := p.CustomFieldSetters[field.Type()]; ok {
|
||||
return f(p, field)
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString("non-zero")
|
||||
case reflect.Bool:
|
||||
field.SetBool(true)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
field.SetInt(7)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
field.SetUint(7)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
field.SetFloat(7)
|
||||
case reflect.Struct:
|
||||
n, err := p.PopulateAll(field)
|
||||
return n + 1, err
|
||||
case reflect.Slice:
|
||||
s := reflect.MakeSlice(field.Type(), 1, 1)
|
||||
_, err := p.setNonZero(s.Index(0))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
field.Set(s)
|
||||
default:
|
||||
return 0, fmt.Errorf("unhandled field kind %s; extend setNonZero", field.Kind())
|
||||
}
|
||||
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func defaultCustomFieldSetters() map[reflect.Type]func(this *PopulateFields, field reflect.Value) (int, error) {
|
||||
return map[reflect.Type]func(this *PopulateFields, field reflect.Value) (int, error){
|
||||
reflect.TypeOf(netip.Prefix{}): func(_ *PopulateFields, field reflect.Value) (int, error) {
|
||||
field.Set(reflect.ValueOf(netip.MustParsePrefix("10.0.0.0/24")))
|
||||
return 1, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PopulateFields) skippedTagPresent(t reflect.StructTag) bool {
|
||||
for tag, value := range p.TagsToSkip {
|
||||
if v := t.Get(tag); v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user