mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-02 15:43:47 -04:00
Compare commits
5 Commits
trigger-pr
...
deploy/sec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5a52711d7 | ||
|
|
b20d484972 | ||
|
|
8931293343 | ||
|
|
7b830d8f72 | ||
|
|
3a0cf230a1 |
@@ -60,8 +60,8 @@
|
||||
|
||||
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
||||
|
||||
### NetBird on Lawrence Systems (Video)
|
||||
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
||||
### Self-Host NetBird (Video)
|
||||
[](https://youtu.be/bZAgpT6nzaQ)
|
||||
|
||||
### Key features
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ import (
|
||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||
"github.com/netbirdio/netbird/client/internal/relay"
|
||||
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||
@@ -141,11 +140,6 @@ type EngineConfig struct {
|
||||
ProfileConfig *profilemanager.Config
|
||||
|
||||
LogPath string
|
||||
|
||||
// ProxyConfig contains system proxy settings for macOS
|
||||
ProxyEnabled bool
|
||||
ProxyHost string
|
||||
ProxyPort int
|
||||
}
|
||||
|
||||
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
||||
@@ -229,9 +223,6 @@ type Engine struct {
|
||||
|
||||
jobExecutor *jobexec.Executor
|
||||
jobExecutorWG sync.WaitGroup
|
||||
|
||||
// proxyManager manages system-wide browser proxy settings on macOS
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// Peer is an instance of the Connection Peer
|
||||
@@ -322,12 +313,6 @@ func (e *Engine) Stop() error {
|
||||
e.updateManager.Stop()
|
||||
}
|
||||
|
||||
if e.proxyManager != nil {
|
||||
if err := e.proxyManager.DisableWebProxy(); err != nil {
|
||||
log.Warnf("failed to disable system proxy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("cleaning up status recorder states")
|
||||
e.statusRecorder.ReplaceOfflinePeers([]peer.State{})
|
||||
e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{})
|
||||
@@ -463,10 +448,6 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
||||
}
|
||||
e.stateManager.Start()
|
||||
|
||||
// Initialize proxy manager and register state for cleanup
|
||||
proxy.RegisterState(e.stateManager)
|
||||
e.proxyManager = proxy.NewManager(e.stateManager)
|
||||
|
||||
initialRoutes, dnsConfig, dnsFeatureFlag, err := e.readInitialSettings()
|
||||
if err != nil {
|
||||
e.close()
|
||||
@@ -1331,9 +1312,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||
// If no server of a server group responds this will disable the respective handler and retry later.
|
||||
e.dnsServer.ProbeAvailability()
|
||||
|
||||
// Update system proxy state based on routes after network map is fully applied
|
||||
e.updateSystemProxy(clientRoutes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2325,26 +2303,6 @@ func createFile(path string) error {
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
// updateSystemProxy triggers a proxy enable/disable cycle after the network map is updated.
|
||||
func (e *Engine) updateSystemProxy(clientRoutes route.HAMap) {
|
||||
if runtime.GOOS != "darwin" || e.proxyManager == nil {
|
||||
log.Errorf("not updating proxy")
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.proxyManager.EnableWebProxy(e.config.ProxyHost, e.config.ProxyPort); err != nil {
|
||||
log.Errorf("enable system proxy: %v", err)
|
||||
return
|
||||
}
|
||||
log.Error("system proxy enabled after network map update")
|
||||
|
||||
if err := e.proxyManager.DisableWebProxy(); err != nil {
|
||||
log.Errorf("disable system proxy: %v", err)
|
||||
return
|
||||
}
|
||||
log.Error("system proxy disabled after network map update")
|
||||
}
|
||||
|
||||
func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) {
|
||||
remoteCred, err := signal.UnMarshalCredential(msg)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||
)
|
||||
|
||||
const networksetupPath = "/usr/sbin/networksetup"
|
||||
|
||||
// Manager handles system-wide proxy configuration on macOS.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
stateManager *statemanager.Manager
|
||||
modifiedServices []string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewManager creates a new proxy manager.
|
||||
func NewManager(stateManager *statemanager.Manager) *Manager {
|
||||
return &Manager{
|
||||
stateManager: stateManager,
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveNetworkServices returns the list of active network services.
|
||||
func GetActiveNetworkServices() ([]string, error) {
|
||||
cmd := exec.Command(networksetupPath, "-listallnetworkservices")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list network services: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
var services []string
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "*") || strings.Contains(line, "asterisk") {
|
||||
continue
|
||||
}
|
||||
services = append(services, line)
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// EnableWebProxy enables web proxy for all active network services.
|
||||
func (m *Manager) EnableWebProxy(host string, port int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.enabled {
|
||||
log.Debug("web proxy already enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
services, err := GetActiveNetworkServices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var modifiedServices []string
|
||||
for _, service := range services {
|
||||
if err := m.enableProxyForService(service, host, port); err != nil {
|
||||
log.Warnf("enable proxy for %s: %v", service, err)
|
||||
continue
|
||||
}
|
||||
modifiedServices = append(modifiedServices, service)
|
||||
}
|
||||
|
||||
m.modifiedServices = modifiedServices
|
||||
m.enabled = true
|
||||
m.updateState()
|
||||
|
||||
log.Infof("enabled web proxy on %d services -> %s:%d", len(modifiedServices), host, port)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) enableProxyForService(service, host string, port int) error {
|
||||
portStr := fmt.Sprintf("%d", port)
|
||||
|
||||
// Set web proxy (HTTP)
|
||||
cmd := exec.Command(networksetupPath, "-setwebproxy", service, host, portStr)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("set web proxy: %w, output: %s", err, out)
|
||||
}
|
||||
|
||||
// Enable web proxy
|
||||
cmd = exec.Command(networksetupPath, "-setwebproxystate", service, "on")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("enable web proxy state: %w, output: %s", err, out)
|
||||
}
|
||||
|
||||
// Set secure web proxy (HTTPS)
|
||||
cmd = exec.Command(networksetupPath, "-setsecurewebproxy", service, host, portStr)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("set secure web proxy: %w, output: %s", err, out)
|
||||
}
|
||||
|
||||
// Enable secure web proxy
|
||||
cmd = exec.Command(networksetupPath, "-setsecurewebproxystate", service, "on")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("enable secure web proxy state: %w, output: %s", err, out)
|
||||
}
|
||||
|
||||
log.Debugf("enabled proxy for service %s", service)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableWebProxy disables web proxy for all modified network services.
|
||||
func (m *Manager) DisableWebProxy() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if !m.enabled {
|
||||
log.Debug("web proxy already disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
services := m.modifiedServices
|
||||
if len(services) == 0 {
|
||||
services, _ = GetActiveNetworkServices()
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if err := m.disableProxyForService(service); err != nil {
|
||||
log.Warnf("disable proxy for %s: %v", service, err)
|
||||
}
|
||||
}
|
||||
|
||||
m.modifiedServices = nil
|
||||
m.enabled = false
|
||||
m.updateState()
|
||||
|
||||
log.Info("disabled web proxy")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) disableProxyForService(service string) error {
|
||||
// Disable web proxy (HTTP)
|
||||
cmd := exec.Command(networksetupPath, "-setwebproxystate", service, "off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("disable web proxy: %w, output: %s", err, out)
|
||||
}
|
||||
|
||||
// Disable secure web proxy (HTTPS)
|
||||
cmd = exec.Command(networksetupPath, "-setsecurewebproxystate", service, "off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("disable secure web proxy: %w, output: %s", err, out)
|
||||
}
|
||||
|
||||
log.Debugf("disabled proxy for service %s", service)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAutoproxyURL sets the automatic proxy configuration URL (PAC file).
|
||||
func (m *Manager) SetAutoproxyURL(pacURL string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
services, err := GetActiveNetworkServices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var modifiedServices []string
|
||||
for _, service := range services {
|
||||
cmd := exec.Command(networksetupPath, "-setautoproxyurl", service, pacURL)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Warnf("set autoproxy for %s: %v, output: %s", service, err, out)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd = exec.Command(networksetupPath, "-setautoproxystate", service, "on")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Warnf("enable autoproxy for %s: %v, output: %s", service, err, out)
|
||||
continue
|
||||
}
|
||||
|
||||
modifiedServices = append(modifiedServices, service)
|
||||
log.Debugf("set autoproxy URL for %s -> %s", service, pacURL)
|
||||
}
|
||||
|
||||
m.modifiedServices = modifiedServices
|
||||
m.enabled = true
|
||||
m.updateState()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableAutoproxy disables automatic proxy configuration.
|
||||
func (m *Manager) DisableAutoproxy() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
services := m.modifiedServices
|
||||
if len(services) == 0 {
|
||||
services, _ = GetActiveNetworkServices()
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
cmd := exec.Command(networksetupPath, "-setautoproxystate", service, "off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Warnf("disable autoproxy for %s: %v, output: %s", service, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
m.modifiedServices = nil
|
||||
m.enabled = false
|
||||
m.updateState()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the proxy is currently enabled.
|
||||
func (m *Manager) IsEnabled() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.enabled
|
||||
}
|
||||
|
||||
// Restore restores proxy settings from a previous state.
|
||||
func (m *Manager) Restore(services []string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, service := range services {
|
||||
if err := m.disableProxyForService(service); err != nil {
|
||||
log.Warnf("restore proxy for %s: %v", service, err)
|
||||
}
|
||||
}
|
||||
|
||||
m.modifiedServices = nil
|
||||
m.enabled = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateState() {
|
||||
if m.stateManager == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if m.enabled && len(m.modifiedServices) > 0 {
|
||||
state := &ShutdownState{
|
||||
ModifiedServices: m.modifiedServices,
|
||||
}
|
||||
if err := m.stateManager.UpdateState(state); err != nil {
|
||||
log.Errorf("update proxy state: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := m.stateManager.DeleteState(&ShutdownState{}); err != nil {
|
||||
log.Debugf("delete proxy state: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//go:build !darwin || ios
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||
)
|
||||
|
||||
// Manager is a no-op proxy manager for non-macOS platforms.
|
||||
type Manager struct{}
|
||||
|
||||
// NewManager creates a new proxy manager (no-op on non-macOS).
|
||||
func NewManager(_ *statemanager.Manager) *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
// EnableWebProxy is a no-op on non-macOS platforms.
|
||||
func (m *Manager) EnableWebProxy(host string, port int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableWebProxy is a no-op on non-macOS platforms.
|
||||
func (m *Manager) DisableWebProxy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAutoproxyURL is a no-op on non-macOS platforms.
|
||||
func (m *Manager) SetAutoproxyURL(pacURL string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableAutoproxy is a no-op on non-macOS platforms.
|
||||
func (m *Manager) DisableAutoproxy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled always returns false on non-macOS platforms.
|
||||
func (m *Manager) IsEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Restore is a no-op on non-macOS platforms.
|
||||
func (m *Manager) Restore(services []string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetActiveNetworkServices(t *testing.T) {
|
||||
services, err := GetActiveNetworkServices()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, services, "should have at least one network service")
|
||||
|
||||
// Check that services don't contain invalid entries
|
||||
for _, service := range services {
|
||||
assert.NotEmpty(t, service)
|
||||
assert.NotContains(t, service, "*")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_EnableDisableWebProxy(t *testing.T) {
|
||||
// Skip this test in CI as it requires admin privileges
|
||||
if testing.Short() {
|
||||
t.Skip("skipping proxy test in short mode")
|
||||
}
|
||||
|
||||
m := NewManager(nil)
|
||||
assert.NotNil(t, m)
|
||||
assert.False(t, m.IsEnabled())
|
||||
|
||||
// This test would require admin privileges to actually enable the proxy
|
||||
// So we just test the basic state management
|
||||
}
|
||||
|
||||
func TestShutdownState_Name(t *testing.T) {
|
||||
state := &ShutdownState{}
|
||||
assert.Equal(t, "proxy_state", state.Name())
|
||||
}
|
||||
|
||||
func TestShutdownState_Cleanup_EmptyServices(t *testing.T) {
|
||||
state := &ShutdownState{
|
||||
ModifiedServices: []string{},
|
||||
}
|
||||
err := state.Cleanup()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
substr string
|
||||
want bool
|
||||
}{
|
||||
{"Enabled: Yes", "Enabled: Yes", true},
|
||||
{"Enabled: No", "Enabled: Yes", false},
|
||||
{"Server: 127.0.0.1\nEnabled: Yes\nPort: 8080", "Enabled: Yes", true},
|
||||
{"", "Enabled: Yes", false},
|
||||
{"Enabled: Yes", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s+"_"+tt.substr, func(t *testing.T) {
|
||||
got := contains(tt.s, tt.substr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProxyEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
output string
|
||||
want bool
|
||||
}{
|
||||
{"Enabled: Yes\nServer: 127.0.0.1\nPort: 8080", true},
|
||||
{"Enabled: No\nServer: \nPort: 0", false},
|
||||
{"Server: 127.0.0.1\nEnabled: Yes\nPort: 8080", true},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.output, func(t *testing.T) {
|
||||
got := isProxyEnabled(tt.output)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||
)
|
||||
|
||||
// ShutdownState stores proxy state for cleanup on unclean shutdown.
|
||||
type ShutdownState struct {
|
||||
ModifiedServices []string `json:"modified_services"`
|
||||
}
|
||||
|
||||
// Name returns the state name for persistence.
|
||||
func (s *ShutdownState) Name() string {
|
||||
return "proxy_state"
|
||||
}
|
||||
|
||||
// Cleanup restores proxy settings after an unclean shutdown.
|
||||
func (s *ShutdownState) Cleanup() error {
|
||||
if len(s.ModifiedServices) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("cleaning up proxy state for %d services", len(s.ModifiedServices))
|
||||
|
||||
for _, service := range s.ModifiedServices {
|
||||
// Disable web proxy (HTTP)
|
||||
cmd := exec.Command(networksetupPath, "-setwebproxystate", service, "off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Warnf("cleanup web proxy for %s: %v, output: %s", service, err, out)
|
||||
}
|
||||
|
||||
// Disable secure web proxy (HTTPS)
|
||||
cmd = exec.Command(networksetupPath, "-setsecurewebproxystate", service, "off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Warnf("cleanup secure web proxy for %s: %v, output: %s", service, err, out)
|
||||
}
|
||||
|
||||
// Disable autoproxy
|
||||
cmd = exec.Command(networksetupPath, "-setautoproxystate", service, "off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Warnf("cleanup autoproxy for %s: %v, output: %s", service, err, out)
|
||||
}
|
||||
|
||||
log.Debugf("cleaned up proxy for service %s", service)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterState registers the proxy state with the state manager.
|
||||
func RegisterState(stateManager *statemanager.Manager) {
|
||||
if stateManager == nil {
|
||||
return
|
||||
}
|
||||
stateManager.RegisterState(&ShutdownState{})
|
||||
}
|
||||
|
||||
// GetProxyState returns the current proxy state from the command line.
|
||||
func GetProxyState(service string) (webProxy, secureProxy, autoProxy bool, err error) {
|
||||
// Check web proxy state
|
||||
cmd := exec.Command(networksetupPath, "-getwebproxy", service)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, false, false, fmt.Errorf("get web proxy: %w", err)
|
||||
}
|
||||
webProxy = isProxyEnabled(string(out))
|
||||
|
||||
// Check secure web proxy state
|
||||
cmd = exec.Command(networksetupPath, "-getsecurewebproxy", service)
|
||||
out, err = cmd.Output()
|
||||
if err != nil {
|
||||
return false, false, false, fmt.Errorf("get secure web proxy: %w", err)
|
||||
}
|
||||
secureProxy = isProxyEnabled(string(out))
|
||||
|
||||
// Check autoproxy state
|
||||
cmd = exec.Command(networksetupPath, "-getautoproxyurl", service)
|
||||
out, err = cmd.Output()
|
||||
if err != nil {
|
||||
return false, false, false, fmt.Errorf("get autoproxy: %w", err)
|
||||
}
|
||||
autoProxy = isProxyEnabled(string(out))
|
||||
|
||||
return webProxy, secureProxy, autoProxy, nil
|
||||
}
|
||||
|
||||
func isProxyEnabled(output string) bool {
|
||||
return !contains(output, "Enabled: No") && contains(output, "Enabled: Yes")
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//go:build !darwin || ios
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||
)
|
||||
|
||||
// ShutdownState is a no-op state for non-macOS platforms.
|
||||
type ShutdownState struct{}
|
||||
|
||||
// Name returns the state name.
|
||||
func (s *ShutdownState) Name() string {
|
||||
return "proxy_state"
|
||||
}
|
||||
|
||||
// Cleanup is a no-op on non-macOS platforms.
|
||||
func (s *ShutdownState) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterState is a no-op on non-macOS platforms.
|
||||
func RegisterState(stateManager *statemanager.Manager) {
|
||||
}
|
||||
@@ -327,6 +327,60 @@ func ensureLocalConnector(ctx context.Context, stor storage.Storage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasNonLocalConnectors checks if there are any connectors other than the local connector.
|
||||
func (p *Provider) HasNonLocalConnectors(ctx context.Context) (bool, error) {
|
||||
connectors, err := p.storage.ListConnectors(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to list connectors: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Info("checking for non-local connectors", "total_connectors", len(connectors))
|
||||
for _, conn := range connectors {
|
||||
p.logger.Info("found connector in storage", "id", conn.ID, "type", conn.Type, "name", conn.Name)
|
||||
if conn.ID != "local" || conn.Type != "local" {
|
||||
p.logger.Info("found non-local connector", "id", conn.ID)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
p.logger.Info("no non-local connectors found")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DisableLocalAuth removes the local (password) connector.
|
||||
// Returns an error if no other connectors are configured.
|
||||
func (p *Provider) DisableLocalAuth(ctx context.Context) error {
|
||||
hasOthers, err := p.HasNonLocalConnectors(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasOthers {
|
||||
return fmt.Errorf("cannot disable local authentication: no other identity providers configured")
|
||||
}
|
||||
|
||||
// Check if local connector exists
|
||||
_, err = p.storage.GetConnector(ctx, "local")
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
// Already disabled
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check local connector: %w", err)
|
||||
}
|
||||
|
||||
// Delete the local connector
|
||||
if err := p.storage.DeleteConnector(ctx, "local"); err != nil {
|
||||
return fmt.Errorf("failed to delete local connector: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Info("local authentication disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableLocalAuth creates the local (password) connector if it doesn't exist.
|
||||
func (p *Provider) EnableLocalAuth(ctx context.Context) error {
|
||||
return ensureLocalConnector(ctx, p.storage)
|
||||
}
|
||||
|
||||
// ensureStaticConnectors creates or updates static connectors in storage
|
||||
func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
|
||||
for _, conn := range connectors {
|
||||
|
||||
@@ -54,6 +54,7 @@ func (s *BaseServer) ProxyController() port_forwarding.Controller {
|
||||
|
||||
func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
||||
return Create(s, func() grpc.SecretsManager {
|
||||
log.Debugf("Initializing secrets manager")
|
||||
secretsManager, err := grpc.NewTimeBasedAuthSecretsManager(s.PeersUpdateManager(), s.Config.TURNConfig, s.Config.Relay, s.SettingsManager(), s.GroupsManager())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create secrets manager: %v", err)
|
||||
|
||||
@@ -69,7 +69,14 @@ func (s *BaseServer) UsersManager() users.Manager {
|
||||
func (s *BaseServer) SettingsManager() settings.Manager {
|
||||
return Create(s, func() settings.Manager {
|
||||
extraSettingsManager := integrations.NewManager(s.EventStore())
|
||||
return settings.NewManager(s.Store(), s.UsersManager(), extraSettingsManager, s.PermissionsManager())
|
||||
|
||||
idpConfig := settings.IdpConfig{}
|
||||
if s.Config.EmbeddedIdP != nil && s.Config.EmbeddedIdP.Enabled {
|
||||
idpConfig.EmbeddedIdpEnabled = true
|
||||
idpConfig.LocalAuthDisabled = s.Config.EmbeddedIdP.LocalAuthDisabled
|
||||
}
|
||||
|
||||
return settings.NewManager(s.Store(), s.UsersManager(), extraSettingsManager, s.PermissionsManager(), idpConfig)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,9 @@ type Server struct {
|
||||
|
||||
oAuthConfigProvider idp.OAuthConfigProvider
|
||||
|
||||
syncSem atomic.Int32
|
||||
syncLim int32
|
||||
syncSem atomic.Int32
|
||||
syncLimEnabled bool
|
||||
syncLim int32
|
||||
}
|
||||
|
||||
// NewServer creates a new Management server
|
||||
@@ -108,6 +109,7 @@ func NewServer(
|
||||
blockPeersWithSameConfig := strings.ToLower(os.Getenv(envBlockPeers)) == "true"
|
||||
|
||||
syncLim := int32(defaultSyncLim)
|
||||
syncLimEnabled := true
|
||||
if syncLimStr := os.Getenv(envConcurrentSyncs); syncLimStr != "" {
|
||||
syncLimParsed, err := strconv.Atoi(syncLimStr)
|
||||
if err != nil {
|
||||
@@ -115,6 +117,9 @@ func NewServer(
|
||||
} else {
|
||||
//nolint:gosec
|
||||
syncLim = int32(syncLimParsed)
|
||||
if syncLim < 0 {
|
||||
syncLimEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +139,8 @@ func NewServer(
|
||||
|
||||
loginFilter: newLoginFilter(),
|
||||
|
||||
syncLim: syncLim,
|
||||
syncLim: syncLim,
|
||||
syncLimEnabled: syncLimEnabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -212,7 +218,7 @@ func (s *Server) Job(srv proto.ManagementService_JobServer) error {
|
||||
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
|
||||
// notifies the connected peer of any updates (e.g. new peers under the same account)
|
||||
func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
||||
if s.syncSem.Load() >= s.syncLim {
|
||||
if s.syncLimEnabled && s.syncSem.Load() >= s.syncLim {
|
||||
return status.Errorf(codes.ResourceExhausted, "too many concurrent sync requests, please try again later")
|
||||
}
|
||||
s.syncSem.Add(1)
|
||||
@@ -305,7 +311,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
||||
s.syncSem.Add(-1)
|
||||
s.cancelPeerRoutines(ctx, accountID, peer)
|
||||
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -313,7 +319,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Debugf("error while notify peer connected for %s: %v", peerKey.String(), err)
|
||||
s.syncSem.Add(-1)
|
||||
s.cancelPeerRoutines(ctx, accountID, peer)
|
||||
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -484,6 +490,10 @@ func (s *Server) cancelPeerRoutines(ctx context.Context, accountID string, peer
|
||||
unlock := s.acquirePeerLockByUID(ctx, peer.Key)
|
||||
defer unlock()
|
||||
|
||||
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||
}
|
||||
|
||||
func (s *Server) cancelPeerRoutinesWithoutLock(ctx context.Context, accountID string, peer *nbpeer.Peer) {
|
||||
err := s.accountManager.OnPeerDisconnected(ctx, accountID, peer.Key)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to disconnect peer %s properly: %v", peer.Key, err)
|
||||
|
||||
@@ -95,6 +95,7 @@ func NewTimeBasedAuthSecretsManager(updateManager network_map.PeersUpdateManager
|
||||
|
||||
// GetWGKey returns WireGuard private key used to generate peer keys
|
||||
func (m *TimeBasedAuthSecretsManager) GetWGKey() (wgtypes.Key, error) {
|
||||
log.Debug("returning wg key from secrets manager")
|
||||
return m.wgKey, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/formatter/hook"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
@@ -49,6 +48,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
@@ -795,6 +795,19 @@ func IsEmbeddedIdp(i idp.Manager) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsLocalAuthDisabled checks if local (email/password) authentication is disabled.
|
||||
// Returns true only when using embedded IDP with local auth disabled in config.
|
||||
func IsLocalAuthDisabled(ctx context.Context, i idp.Manager) bool {
|
||||
if isNil(i) {
|
||||
return false
|
||||
}
|
||||
embeddedIdp, ok := i.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return embeddedIdp.IsLocalAuthDisabled()
|
||||
}
|
||||
|
||||
// addAccountIDToIDPAppMeta update user's app metadata in idp manager
|
||||
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
|
||||
if !isNil(am.idpManager) && !IsEmbeddedIdp(am.idpManager) {
|
||||
|
||||
@@ -129,14 +129,14 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
|
||||
return nil, fmt.Errorf("register integrations endpoints: %w", err)
|
||||
}
|
||||
|
||||
// Check if embedded IdP is enabled
|
||||
// Check if embedded IdP is enabled for instance manager
|
||||
embeddedIdP, embeddedIdpEnabled := idpManager.(*idpmanager.EmbeddedIdPManager)
|
||||
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
||||
}
|
||||
|
||||
accounts.AddEndpoints(accountManager, settingsManager, embeddedIdpEnabled, router)
|
||||
accounts.AddEndpoints(accountManager, settingsManager, router)
|
||||
peers.AddEndpoints(accountManager, router, networkMapController)
|
||||
users.AddEndpoints(accountManager, router)
|
||||
users.AddInvitesEndpoints(accountManager, router)
|
||||
|
||||
@@ -36,24 +36,22 @@ const (
|
||||
|
||||
// handler is a handler that handles the server.Account HTTP endpoints
|
||||
type handler struct {
|
||||
accountManager account.Manager
|
||||
settingsManager settings.Manager
|
||||
embeddedIdpEnabled bool
|
||||
accountManager account.Manager
|
||||
settingsManager settings.Manager
|
||||
}
|
||||
|
||||
func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, embeddedIdpEnabled bool, router *mux.Router) {
|
||||
accountsHandler := newHandler(accountManager, settingsManager, embeddedIdpEnabled)
|
||||
func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, router *mux.Router) {
|
||||
accountsHandler := newHandler(accountManager, settingsManager)
|
||||
router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
|
||||
router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
|
||||
router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
|
||||
}
|
||||
|
||||
// newHandler creates a new handler HTTP handler
|
||||
func newHandler(accountManager account.Manager, settingsManager settings.Manager, embeddedIdpEnabled bool) *handler {
|
||||
func newHandler(accountManager account.Manager, settingsManager settings.Manager) *handler {
|
||||
return &handler{
|
||||
accountManager: accountManager,
|
||||
settingsManager: settingsManager,
|
||||
embeddedIdpEnabled: embeddedIdpEnabled,
|
||||
accountManager: accountManager,
|
||||
settingsManager: settingsManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +163,7 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := toAccountResponse(accountID, settings, meta, onboarding, h.embeddedIdpEnabled)
|
||||
resp := toAccountResponse(accountID, settings, meta, onboarding)
|
||||
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
|
||||
}
|
||||
|
||||
@@ -292,7 +290,7 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding, h.embeddedIdpEnabled)
|
||||
resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding)
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, &resp)
|
||||
}
|
||||
@@ -321,7 +319,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||
}
|
||||
|
||||
func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta, onboarding *types.AccountOnboarding, embeddedIdpEnabled bool) *api.Account {
|
||||
func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta, onboarding *types.AccountOnboarding) *api.Account {
|
||||
jwtAllowGroups := settings.JWTAllowGroups
|
||||
if jwtAllowGroups == nil {
|
||||
jwtAllowGroups = []string{}
|
||||
@@ -341,7 +339,8 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
||||
LazyConnectionEnabled: &settings.LazyConnectionEnabled,
|
||||
DnsDomain: &settings.DNSDomain,
|
||||
AutoUpdateVersion: &settings.AutoUpdateVersion,
|
||||
EmbeddedIdpEnabled: &embeddedIdpEnabled,
|
||||
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
||||
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
||||
}
|
||||
|
||||
if settings.NetworkRange.IsValid() {
|
||||
|
||||
@@ -33,7 +33,6 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler {
|
||||
AnyTimes()
|
||||
|
||||
return &handler{
|
||||
embeddedIdpEnabled: false,
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
||||
return account.Settings, nil
|
||||
@@ -124,6 +123,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
},
|
||||
expectedArray: true,
|
||||
expectedID: accountID,
|
||||
@@ -148,6 +148,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -172,6 +173,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr("latest"),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -196,6 +198,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -220,6 +223,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -244,6 +248,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
DnsDomain: sr(""),
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
|
||||
@@ -46,7 +46,7 @@ func (h *handler) getInstanceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
util.WriteErrorResponse("failed to check instance status", http.StatusInternalServerError, w)
|
||||
return
|
||||
}
|
||||
|
||||
log.WithContext(r.Context()).Infof("instance setup status: %v", setupRequired)
|
||||
util.WriteJSONObject(r.Context(), w, api.InstanceStatus{
|
||||
SetupRequired: setupRequired,
|
||||
})
|
||||
|
||||
@@ -205,6 +205,14 @@ func TestCreateInvite(t *testing.T) {
|
||||
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local auth disabled",
|
||||
requestBody: `{"email":"test@example.com","name":"Test User","role":"user","auto_groups":[]}`,
|
||||
expectedStatus: http.StatusPreconditionFailed,
|
||||
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
||||
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
requestBody: `{invalid json}`,
|
||||
@@ -376,6 +384,15 @@ func TestAcceptInvite(t *testing.T) {
|
||||
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local auth disabled",
|
||||
token: testInviteToken,
|
||||
requestBody: `{"password":"SecurePass123!"}`,
|
||||
expectedStatus: http.StatusPreconditionFailed,
|
||||
mockFunc: func(ctx context.Context, token, password string) error {
|
||||
return status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing token",
|
||||
token: "",
|
||||
|
||||
@@ -73,7 +73,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
proxyController := integrations.NewController(store)
|
||||
userManager := users.NewManager(store)
|
||||
permissionsManager := permissions.NewManager(store)
|
||||
settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager)
|
||||
settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager, settings.IdpConfig{})
|
||||
peersManager := peers.NewManager(store, permissionsManager)
|
||||
|
||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||
|
||||
@@ -43,6 +43,11 @@ type EmbeddedIdPConfig struct {
|
||||
Owner *OwnerConfig
|
||||
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
|
||||
SignKeyRefreshEnabled bool
|
||||
// LocalAuthDisabled disables the local (email/password) authentication connector.
|
||||
// When true, users cannot authenticate via email/password, only via external identity providers.
|
||||
// Existing local users are preserved and will be able to login again if re-enabled.
|
||||
// Cannot be enabled if no external identity provider connectors are configured.
|
||||
LocalAuthDisabled bool
|
||||
}
|
||||
|
||||
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
|
||||
@@ -105,6 +110,8 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
Issuer: "NetBird",
|
||||
Theme: "light",
|
||||
},
|
||||
// Always enable password DB initially - we disable the local connector after startup if needed.
|
||||
// This ensures Dex has at least one connector during initialization.
|
||||
EnablePasswordDB: true,
|
||||
StaticClients: []storage.Client{
|
||||
{
|
||||
@@ -192,11 +199,32 @@ func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v", config)
|
||||
|
||||
provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create embedded IdP provider: %w", err)
|
||||
}
|
||||
|
||||
// If local auth is disabled, validate that other connectors exist
|
||||
if config.LocalAuthDisabled {
|
||||
hasOthers, err := provider.HasNonLocalConnectors(ctx)
|
||||
if err != nil {
|
||||
_ = provider.Stop(ctx)
|
||||
return nil, fmt.Errorf("failed to check connectors: %w", err)
|
||||
}
|
||||
if !hasOthers {
|
||||
_ = provider.Stop(ctx)
|
||||
return nil, fmt.Errorf("cannot disable local authentication: no other identity providers configured")
|
||||
}
|
||||
// Ensure local connector is removed (it might exist from a previous run)
|
||||
if err := provider.DisableLocalAuth(ctx); err != nil {
|
||||
_ = provider.Stop(ctx)
|
||||
return nil, fmt.Errorf("failed to disable local auth: %w", err)
|
||||
}
|
||||
log.WithContext(ctx).Info("local authentication disabled - only external identity providers can be used")
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Infof("embedded Dex IDP initialized with issuer: %s", yamlConfig.Issuer)
|
||||
|
||||
return &EmbeddedIdPManager{
|
||||
@@ -281,6 +309,8 @@ func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]*
|
||||
return nil, fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("retrieved %d users from embedded IdP", len(users))
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, user := range users {
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], &UserData{
|
||||
@@ -290,11 +320,17 @@ func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]*
|
||||
})
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("retrieved %d users from embedded IdP", len(indexedUsers[UnsetAccountID]))
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in the embedded IdP.
|
||||
func (m *EmbeddedIdPManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
if m.config.LocalAuthDisabled {
|
||||
return nil, fmt.Errorf("local user creation is disabled")
|
||||
}
|
||||
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
@@ -364,6 +400,10 @@ func (m *EmbeddedIdPManager) GetUserByEmail(ctx context.Context, email string) (
|
||||
// Unlike CreateUser which auto-generates a password, this method uses the provided password.
|
||||
// This is useful for instance setup where the user provides their own password.
|
||||
func (m *EmbeddedIdPManager) CreateUserWithPassword(ctx context.Context, email, password, name string) (*UserData, error) {
|
||||
if m.config.LocalAuthDisabled {
|
||||
return nil, fmt.Errorf("local user creation is disabled")
|
||||
}
|
||||
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
@@ -553,3 +593,13 @@ func (m *EmbeddedIdPManager) GetClientIDs() []string {
|
||||
func (m *EmbeddedIdPManager) GetUserIDClaim() string {
|
||||
return defaultUserIDClaim
|
||||
}
|
||||
|
||||
// IsLocalAuthDisabled returns whether local authentication is disabled based on configuration.
|
||||
func (m *EmbeddedIdPManager) IsLocalAuthDisabled() bool {
|
||||
return m.config.LocalAuthDisabled
|
||||
}
|
||||
|
||||
// HasNonLocalConnectors checks if there are any identity provider connectors other than local.
|
||||
func (m *EmbeddedIdPManager) HasNonLocalConnectors(ctx context.Context) (bool, error) {
|
||||
return m.provider.HasNonLocalConnectors(ctx)
|
||||
}
|
||||
|
||||
@@ -370,3 +370,234 @@ func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("cannot start with local auth disabled without other connectors", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
config := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
LocalAuthDisabled: true,
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: filepath.Join(tmpDir, "dex.db"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = NewEmbeddedIdPManager(ctx, config, nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no other identity providers configured")
|
||||
})
|
||||
|
||||
t.Run("local auth enabled by default", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
config := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: filepath.Join(tmpDir, "dex.db"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = manager.Stop(ctx) }()
|
||||
|
||||
// Verify local auth is enabled by default
|
||||
assert.False(t, manager.IsLocalAuthDisabled())
|
||||
})
|
||||
|
||||
t.Run("start with local auth disabled when connector exists", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbFile := filepath.Join(tmpDir, "dex.db")
|
||||
|
||||
// First, create a manager with local auth enabled and add a connector
|
||||
config1 := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: dbFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a user
|
||||
userData, err := manager1.CreateUser(ctx, "preserved@example.com", "Preserved User", "account1", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
userID := userData.ID
|
||||
|
||||
// Add an external connector (Google doesn't require OIDC discovery)
|
||||
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
|
||||
ID: "google-test",
|
||||
Name: "Google Test",
|
||||
Type: "google",
|
||||
ClientID: "test-client-id",
|
||||
ClientSecret: "test-client-secret",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Stop the first manager
|
||||
err = manager1.Stop(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now create a new manager with local auth disabled
|
||||
config2 := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
LocalAuthDisabled: true,
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: dbFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = manager2.Stop(ctx) }()
|
||||
|
||||
// Verify local auth is disabled via config
|
||||
assert.True(t, manager2.IsLocalAuthDisabled())
|
||||
|
||||
// Verify the user still exists in storage (just can't login via local)
|
||||
lookedUp, err := manager2.GetUserDataByID(ctx, userID, AppMetadata{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "preserved@example.com", lookedUp.Email)
|
||||
})
|
||||
|
||||
t.Run("CreateUser fails when local auth is disabled", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbFile := filepath.Join(tmpDir, "dex.db")
|
||||
|
||||
// First, create a manager and add an external connector
|
||||
config1 := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: dbFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
|
||||
ID: "google-test",
|
||||
Name: "Google Test",
|
||||
Type: "google",
|
||||
ClientID: "test-client-id",
|
||||
ClientSecret: "test-client-secret",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager1.Stop(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create manager with local auth disabled
|
||||
config2 := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
LocalAuthDisabled: true,
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: dbFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = manager2.Stop(ctx) }()
|
||||
|
||||
// Try to create a user - should fail
|
||||
_, err = manager2.CreateUser(ctx, "newuser@example.com", "New User", "account1", "admin@example.com")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "local user creation is disabled")
|
||||
})
|
||||
|
||||
t.Run("CreateUserWithPassword fails when local auth is disabled", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbFile := filepath.Join(tmpDir, "dex.db")
|
||||
|
||||
// First, create a manager and add an external connector
|
||||
config1 := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: dbFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
|
||||
ID: "google-test",
|
||||
Name: "Google Test",
|
||||
Type: "google",
|
||||
ClientID: "test-client-id",
|
||||
ClientSecret: "test-client-secret",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager1.Stop(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create manager with local auth disabled
|
||||
config2 := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
LocalAuthDisabled: true,
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: dbFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = manager2.Stop(ctx) }()
|
||||
|
||||
// Try to create a user with password - should fail
|
||||
_, err = manager2.CreateUserWithPassword(ctx, "newuser@example.com", "SecurePass123!", "New User")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "local user creation is disabled")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,13 +104,22 @@ func NewManager(ctx context.Context, store store.Store, idpManager idp.Manager)
|
||||
}
|
||||
|
||||
func (m *DefaultManager) loadSetupRequired(ctx context.Context) error {
|
||||
// Check if there are any accounts in the NetBird store
|
||||
numAccounts, err := m.store.GetAccountsCounter(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasAccounts := numAccounts > 0
|
||||
|
||||
// Check if there are any users in the embedded IdP (Dex)
|
||||
users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasLocalUsers := len(users) > 0
|
||||
|
||||
m.setupMu.Lock()
|
||||
m.setupRequired = len(users) == 0
|
||||
m.setupRequired = !(hasAccounts || hasLocalUsers)
|
||||
m.setupMu.Unlock()
|
||||
|
||||
return nil
|
||||
|
||||
@@ -24,19 +24,28 @@ type Manager interface {
|
||||
UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error)
|
||||
}
|
||||
|
||||
// IdpConfig holds IdP-related configuration that is set at runtime
|
||||
// and not stored in the database.
|
||||
type IdpConfig struct {
|
||||
EmbeddedIdpEnabled bool
|
||||
LocalAuthDisabled bool
|
||||
}
|
||||
|
||||
type managerImpl struct {
|
||||
store store.Store
|
||||
extraSettingsManager extra_settings.Manager
|
||||
userManager users.Manager
|
||||
permissionsManager permissions.Manager
|
||||
idpConfig IdpConfig
|
||||
}
|
||||
|
||||
func NewManager(store store.Store, userManager users.Manager, extraSettingsManager extra_settings.Manager, permissionsManager permissions.Manager) Manager {
|
||||
func NewManager(store store.Store, userManager users.Manager, extraSettingsManager extra_settings.Manager, permissionsManager permissions.Manager, idpConfig IdpConfig) Manager {
|
||||
return &managerImpl{
|
||||
store: store,
|
||||
extraSettingsManager: extraSettingsManager,
|
||||
userManager: userManager,
|
||||
permissionsManager: permissionsManager,
|
||||
idpConfig: idpConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +83,10 @@ func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string)
|
||||
settings.Extra.FlowDnsCollectionEnabled = extraSettings.FlowDnsCollectionEnabled
|
||||
}
|
||||
|
||||
// Fill in IdP-related runtime settings
|
||||
settings.EmbeddedIdpEnabled = m.idpConfig.EmbeddedIdpEnabled
|
||||
settings.LocalAuthDisabled = m.idpConfig.LocalAuthDisabled
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ type Settings struct {
|
||||
|
||||
// AutoUpdateVersion client auto-update version
|
||||
AutoUpdateVersion string `gorm:"default:'disabled'"`
|
||||
|
||||
// EmbeddedIdpEnabled indicates if the embedded identity provider is enabled.
|
||||
// This is a runtime-only field, not stored in the database.
|
||||
EmbeddedIdpEnabled bool `gorm:"-"`
|
||||
|
||||
// LocalAuthDisabled indicates if local (email/password) authentication is disabled.
|
||||
// This is a runtime-only field, not stored in the database.
|
||||
LocalAuthDisabled bool `gorm:"-"`
|
||||
}
|
||||
|
||||
// Copy copies the Settings struct
|
||||
@@ -76,6 +84,8 @@ func (s *Settings) Copy() *Settings {
|
||||
DNSDomain: s.DNSDomain,
|
||||
NetworkRange: s.NetworkRange,
|
||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
||||
LocalAuthDisabled: s.LocalAuthDisabled,
|
||||
}
|
||||
if s.Extra != nil {
|
||||
settings.Extra = s.Extra.Copy()
|
||||
|
||||
@@ -191,6 +191,10 @@ func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID
|
||||
// Unlike createNewIdpUser, this method fetches user data directly from the database
|
||||
// since the embedded IdP usage ensures the username and email are stored locally in the User table.
|
||||
func (am *DefaultAccountManager) createEmbeddedIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) {
|
||||
if IsLocalAuthDisabled(ctx, am.idpManager) {
|
||||
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||
}
|
||||
|
||||
inviter, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, inviterID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get inviter user: %w", err)
|
||||
@@ -1462,6 +1466,10 @@ func (am *DefaultAccountManager) CreateUserInvite(ctx context.Context, accountID
|
||||
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||
}
|
||||
|
||||
if IsLocalAuthDisabled(ctx, am.idpManager) {
|
||||
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||
}
|
||||
|
||||
if err := validateUserInvite(invite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1621,6 +1629,10 @@ func (am *DefaultAccountManager) AcceptUserInvite(ctx context.Context, token, pa
|
||||
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||
}
|
||||
|
||||
if IsLocalAuthDisabled(ctx, am.idpManager) {
|
||||
return status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return status.Errorf(status.InvalidArgument, "password is required")
|
||||
}
|
||||
|
||||
@@ -294,6 +294,11 @@ components:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: false
|
||||
local_auth_disabled:
|
||||
description: Indicates whether local (email/password) authentication is disabled. When true, users can only authenticate via external identity providers. This is a read-only field.
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: false
|
||||
required:
|
||||
- peer_login_expiration_enabled
|
||||
- peer_login_expiration
|
||||
|
||||
@@ -415,6 +415,9 @@ type AccountSettings struct {
|
||||
// LazyConnectionEnabled Enables or disables experimental lazy connection
|
||||
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
|
||||
|
||||
// LocalAuthDisabled Indicates whether local (email/password) authentication is disabled. When true, users can only authenticate via external identity providers. This is a read-only field.
|
||||
LocalAuthDisabled *bool `json:"local_auth_disabled,omitempty"`
|
||||
|
||||
// NetworkRange Allows to define a custom network range for the account in CIDR format
|
||||
NetworkRange *string `json:"network_range,omitempty"`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user