Compare commits
5 Commits
fix/win-dn
...
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
|
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
||||||
|
|
||||||
### NetBird on Lawrence Systems (Video)
|
### Self-Host NetBird (Video)
|
||||||
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
[](https://youtu.be/bZAgpT6nzaQ)
|
||||||
|
|
||||||
### Key features
|
### Key features
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ const (
|
|||||||
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
|
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
|
||||||
dnsPolicyConfigConfigOptionsValue = 0x8
|
dnsPolicyConfigConfigOptionsValue = 0x8
|
||||||
|
|
||||||
// NRPT rules cannot handle more than 50 domains per rule.
|
|
||||||
// This is an undocumented Windows limitation.
|
|
||||||
nrptMaxDomainsPerRule = 50
|
|
||||||
|
|
||||||
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
|
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
|
||||||
interfaceConfigNameServerKey = "NameServer"
|
interfaceConfigNameServerKey = "NameServer"
|
||||||
interfaceConfigSearchListKey = "SearchList"
|
interfaceConfigSearchListKey = "SearchList"
|
||||||
@@ -243,32 +239,23 @@ func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error {
|
|||||||
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) (int, error) {
|
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) (int, error) {
|
||||||
// if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored
|
// if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored
|
||||||
// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745
|
// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745
|
||||||
|
for i, domain := range domains {
|
||||||
|
localPath := fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i)
|
||||||
|
gpoPath := fmt.Sprintf("%s-%d", gpoDnsPolicyConfigMatchPath, i)
|
||||||
|
|
||||||
// NRPT rules have an undocumented restriction: each rule can only handle up to 50 domains.
|
singleDomain := []string{domain}
|
||||||
// We need to batch domains into chunks and create one NRPT rule per batch.
|
|
||||||
ruleIndex := 0
|
|
||||||
for i := 0; i < len(domains); i += nrptMaxDomainsPerRule {
|
|
||||||
end := i + nrptMaxDomainsPerRule
|
|
||||||
if end > len(domains) {
|
|
||||||
end = len(domains)
|
|
||||||
}
|
|
||||||
batchDomains := domains[i:end]
|
|
||||||
|
|
||||||
localPath := fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, ruleIndex)
|
if err := r.configureDNSPolicy(localPath, singleDomain, ip); err != nil {
|
||||||
gpoPath := fmt.Sprintf("%s-%d", gpoDnsPolicyConfigMatchPath, ruleIndex)
|
return i, fmt.Errorf("configure DNS Local policy for domain %s: %w", domain, err)
|
||||||
|
|
||||||
if err := r.configureDNSPolicy(localPath, batchDomains, ip); err != nil {
|
|
||||||
return ruleIndex, fmt.Errorf("configure DNS Local policy for rule %d: %w", ruleIndex, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.gpo {
|
if r.gpo {
|
||||||
if err := r.configureDNSPolicy(gpoPath, batchDomains, ip); err != nil {
|
if err := r.configureDNSPolicy(gpoPath, singleDomain, ip); err != nil {
|
||||||
return ruleIndex, fmt.Errorf("configure gpo DNS policy for rule %d: %w", ruleIndex, err)
|
return i, fmt.Errorf("configure gpo DNS policy: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("added NRPT rule %d with %d domains", ruleIndex, len(batchDomains))
|
log.Debugf("added NRPT entry for domain: %s", domain)
|
||||||
ruleIndex++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.gpo {
|
if r.gpo {
|
||||||
@@ -277,8 +264,8 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("added %d NRPT rules for %d domains. Domain list: %s", ruleIndex, len(domains), domains)
|
log.Infof("added %d separate NRPT entries. Domain list: %s", len(domains), domains)
|
||||||
return ruleIndex, nil
|
return len(domains), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error {
|
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error {
|
||||||
|
|||||||
@@ -97,107 +97,6 @@ func registryKeyExists(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanupRegistryKeys(*testing.T) {
|
func cleanupRegistryKeys(*testing.T) {
|
||||||
// Clean up more entries to account for batching tests with many domains
|
cfg := ®istryConfigurator{nrptEntryCount: 10}
|
||||||
cfg := ®istryConfigurator{nrptEntryCount: 20}
|
|
||||||
_ = cfg.removeDNSMatchPolicies()
|
_ = cfg.removeDNSMatchPolicies()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNRPTDomainBatching verifies that domains are correctly batched into NRPT rules
|
|
||||||
// with a maximum of 50 domains per rule (Windows limitation).
|
|
||||||
func TestNRPTDomainBatching(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping registry integration test in short mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer cleanupRegistryKeys(t)
|
|
||||||
cleanupRegistryKeys(t)
|
|
||||||
|
|
||||||
testIP := netip.MustParseAddr("100.64.0.1")
|
|
||||||
|
|
||||||
// Create a test interface registry key so updateSearchDomains doesn't fail
|
|
||||||
testGUID := "{12345678-1234-1234-1234-123456789ABC}"
|
|
||||||
interfacePath := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + testGUID
|
|
||||||
testKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, interfacePath, registry.SET_VALUE)
|
|
||||||
require.NoError(t, err, "Should create test interface registry key")
|
|
||||||
testKey.Close()
|
|
||||||
defer func() {
|
|
||||||
_ = registry.DeleteKey(registry.LOCAL_MACHINE, interfacePath)
|
|
||||||
}()
|
|
||||||
|
|
||||||
cfg := ®istryConfigurator{
|
|
||||||
guid: testGUID,
|
|
||||||
gpo: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
domainCount int
|
|
||||||
expectedRuleCount int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Less than 50 domains (single rule)",
|
|
||||||
domainCount: 30,
|
|
||||||
expectedRuleCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Exactly 50 domains (single rule)",
|
|
||||||
domainCount: 50,
|
|
||||||
expectedRuleCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "51 domains (two rules)",
|
|
||||||
domainCount: 51,
|
|
||||||
expectedRuleCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "100 domains (two rules)",
|
|
||||||
domainCount: 100,
|
|
||||||
expectedRuleCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "125 domains (three rules: 50+50+25)",
|
|
||||||
domainCount: 125,
|
|
||||||
expectedRuleCount: 3,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Clean up before each subtest
|
|
||||||
cleanupRegistryKeys(t)
|
|
||||||
|
|
||||||
// Generate domains
|
|
||||||
domains := make([]DomainConfig, tc.domainCount)
|
|
||||||
for i := 0; i < tc.domainCount; i++ {
|
|
||||||
domains[i] = DomainConfig{
|
|
||||||
Domain: fmt.Sprintf("domain%d.com", i+1),
|
|
||||||
MatchOnly: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config := HostDNSConfig{
|
|
||||||
ServerIP: testIP,
|
|
||||||
Domains: domains,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := cfg.applyDNSConfig(config, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify that exactly expectedRuleCount rules were created
|
|
||||||
assert.Equal(t, tc.expectedRuleCount, cfg.nrptEntryCount,
|
|
||||||
"Should create %d NRPT rules for %d domains", tc.expectedRuleCount, tc.domainCount)
|
|
||||||
|
|
||||||
// Verify all expected rules exist
|
|
||||||
for i := 0; i < tc.expectedRuleCount; i++ {
|
|
||||||
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, exists, "NRPT rule %d should exist", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify no extra rules were created
|
|
||||||
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, tc.expectedRuleCount))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, exists, "No NRPT rule should exist at index %d", tc.expectedRuleCount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -84,13 +84,3 @@ func (m *MockServer) UpdateServerConfig(domains dnsconfig.ServerDomains) error {
|
|||||||
func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeginBatch mock implementation of BeginBatch from Server interface
|
|
||||||
func (m *MockServer) BeginBatch() {
|
|
||||||
// Mock implementation - no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndBatch mock implementation of EndBatch from Server interface
|
|
||||||
func (m *MockServer) EndBatch() {
|
|
||||||
// Mock implementation - no-op
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ type IosDnsManager interface {
|
|||||||
type Server interface {
|
type Server interface {
|
||||||
RegisterHandler(domains domain.List, handler dns.Handler, priority int)
|
RegisterHandler(domains domain.List, handler dns.Handler, priority int)
|
||||||
DeregisterHandler(domains domain.List, priority int)
|
DeregisterHandler(domains domain.List, priority int)
|
||||||
BeginBatch()
|
|
||||||
EndBatch()
|
|
||||||
Initialize() error
|
Initialize() error
|
||||||
Stop()
|
Stop()
|
||||||
DnsIP() netip.Addr
|
DnsIP() netip.Addr
|
||||||
@@ -85,7 +83,6 @@ type DefaultServer struct {
|
|||||||
currentConfigHash uint64
|
currentConfigHash uint64
|
||||||
handlerChain *HandlerChain
|
handlerChain *HandlerChain
|
||||||
extraDomains map[domain.Domain]int
|
extraDomains map[domain.Domain]int
|
||||||
batchMode bool
|
|
||||||
|
|
||||||
mgmtCacheResolver *mgmt.Resolver
|
mgmtCacheResolver *mgmt.Resolver
|
||||||
|
|
||||||
@@ -233,9 +230,7 @@ func (s *DefaultServer) RegisterHandler(domains domain.List, handler dns.Handler
|
|||||||
// convert to zone with simple ref counter
|
// convert to zone with simple ref counter
|
||||||
s.extraDomains[toZone(domain)]++
|
s.extraDomains[toZone(domain)]++
|
||||||
}
|
}
|
||||||
if !s.batchMode {
|
s.applyHostConfig()
|
||||||
s.applyHostConfig()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
||||||
@@ -264,28 +259,6 @@ func (s *DefaultServer) DeregisterHandler(domains domain.List, priority int) {
|
|||||||
delete(s.extraDomains, zone)
|
delete(s.extraDomains, zone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !s.batchMode {
|
|
||||||
s.applyHostConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeginBatch starts batch mode for DNS handler registration/deregistration.
|
|
||||||
// In batch mode, applyHostConfig() is not called after each handler operation,
|
|
||||||
// allowing multiple handlers to be registered/deregistered efficiently.
|
|
||||||
// Must be followed by EndBatch() to apply the accumulated changes.
|
|
||||||
func (s *DefaultServer) BeginBatch() {
|
|
||||||
s.mux.Lock()
|
|
||||||
defer s.mux.Unlock()
|
|
||||||
log.Infof("DNS batch mode enabled")
|
|
||||||
s.batchMode = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndBatch ends batch mode and applies all accumulated DNS configuration changes.
|
|
||||||
func (s *DefaultServer) EndBatch() {
|
|
||||||
s.mux.Lock()
|
|
||||||
defer s.mux.Unlock()
|
|
||||||
log.Infof("DNS batch mode disabled, applying accumulated changes")
|
|
||||||
s.batchMode = false
|
|
||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,9 +508,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
s.currentConfig.RouteAll = false
|
s.currentConfig.RouteAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.batchMode {
|
s.applyHostConfig()
|
||||||
s.applyHostConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.shutdownWg.Add(1)
|
s.shutdownWg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -901,9 +872,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.batchMode {
|
s.applyHostConfig()
|
||||||
s.applyHostConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.stateManager.PersistState(s.ctx); err != nil {
|
if err := s.stateManager.PersistState(s.ctx); err != nil {
|
||||||
@@ -938,9 +907,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
s.registerHandler([]string{nbdns.RootZone}, handler, priority)
|
s.registerHandler([]string{nbdns.RootZone}, handler, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.batchMode {
|
s.applyHostConfig()
|
||||||
s.applyHostConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.updateNSState(nsGroup, nil, true)
|
s.updateNSState(nsGroup, nil, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ func TestGetServerDns(t *testing.T) {
|
|||||||
t.Errorf("invalid dns server instance: %s", err)
|
t.Errorf("invalid dns server instance: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mockSrvB, ok := srvB.(*MockServer)
|
if srvB != srv {
|
||||||
if !ok {
|
|
||||||
t.Errorf("returned server is not a MockServer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mockSrvB != srv {
|
|
||||||
t.Errorf("mismatch dns instances")
|
t.Errorf("mismatch dns instances")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -828,10 +828,6 @@ func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdate
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||||
started := time.Now()
|
|
||||||
defer func() {
|
|
||||||
log.Infof("sync finished in %s", time.Since(started))
|
|
||||||
}()
|
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -173,21 +173,12 @@ func (m *DefaultManager) setupAndroidRoutes(config ManagerConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) setupRefCounters(useNoop bool) {
|
func (m *DefaultManager) setupRefCounters(useNoop bool) {
|
||||||
var once sync.Once
|
|
||||||
var wgIface *net.Interface
|
|
||||||
toInterface := func() *net.Interface {
|
|
||||||
once.Do(func() {
|
|
||||||
wgIface = m.wgInterface.ToInterface()
|
|
||||||
})
|
|
||||||
return wgIface
|
|
||||||
}
|
|
||||||
|
|
||||||
m.routeRefCounter = refcounter.New(
|
m.routeRefCounter = refcounter.New(
|
||||||
func(prefix netip.Prefix, _ struct{}) (struct{}, error) {
|
func(prefix netip.Prefix, _ struct{}) (struct{}, error) {
|
||||||
return struct{}{}, m.sysOps.AddVPNRoute(prefix, toInterface())
|
return struct{}{}, m.sysOps.AddVPNRoute(prefix, m.wgInterface.ToInterface())
|
||||||
},
|
},
|
||||||
func(prefix netip.Prefix, _ struct{}) error {
|
func(prefix netip.Prefix, _ struct{}) error {
|
||||||
return m.sysOps.RemoveVPNRoute(prefix, toInterface())
|
return m.sysOps.RemoveVPNRoute(prefix, m.wgInterface.ToInterface())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -346,13 +337,6 @@ func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
// Begin batch mode to avoid calling applyHostConfig() after each DNS handler operation
|
|
||||||
if m.dnsServer != nil {
|
|
||||||
m.dnsServer.BeginBatch()
|
|
||||||
defer m.dnsServer.EndBatch()
|
|
||||||
}
|
|
||||||
|
|
||||||
for id, handler := range toRemove {
|
for id, handler := range toRemove {
|
||||||
if err := handler.RemoveRoute(); err != nil {
|
if err := handler.RemoveRoute(); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("remove route %s: %w", handler.String(), err))
|
merr = multierror.Append(merr, fmt.Errorf("remove route %s: %w", handler.String(), err))
|
||||||
|
|||||||
@@ -327,6 +327,60 @@ func ensureLocalConnector(ctx context.Context, stor storage.Storage) error {
|
|||||||
return nil
|
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
|
// ensureStaticConnectors creates or updates static connectors in storage
|
||||||
func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
|
func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
|
||||||
for _, conn := range connectors {
|
for _, conn := range connectors {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (s *BaseServer) ProxyController() port_forwarding.Controller {
|
|||||||
|
|
||||||
func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
||||||
return Create(s, func() 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())
|
secretsManager, err := grpc.NewTimeBasedAuthSecretsManager(s.PeersUpdateManager(), s.Config.TURNConfig, s.Config.Relay, s.SettingsManager(), s.GroupsManager())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create secrets manager: %v", err)
|
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 {
|
func (s *BaseServer) SettingsManager() settings.Manager {
|
||||||
return Create(s, func() settings.Manager {
|
return Create(s, func() settings.Manager {
|
||||||
extraSettingsManager := integrations.NewManager(s.EventStore())
|
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
|
oAuthConfigProvider idp.OAuthConfigProvider
|
||||||
|
|
||||||
syncSem atomic.Int32
|
syncSem atomic.Int32
|
||||||
syncLim int32
|
syncLimEnabled bool
|
||||||
|
syncLim int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Management server
|
// NewServer creates a new Management server
|
||||||
@@ -108,6 +109,7 @@ func NewServer(
|
|||||||
blockPeersWithSameConfig := strings.ToLower(os.Getenv(envBlockPeers)) == "true"
|
blockPeersWithSameConfig := strings.ToLower(os.Getenv(envBlockPeers)) == "true"
|
||||||
|
|
||||||
syncLim := int32(defaultSyncLim)
|
syncLim := int32(defaultSyncLim)
|
||||||
|
syncLimEnabled := true
|
||||||
if syncLimStr := os.Getenv(envConcurrentSyncs); syncLimStr != "" {
|
if syncLimStr := os.Getenv(envConcurrentSyncs); syncLimStr != "" {
|
||||||
syncLimParsed, err := strconv.Atoi(syncLimStr)
|
syncLimParsed, err := strconv.Atoi(syncLimStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,6 +117,9 @@ func NewServer(
|
|||||||
} else {
|
} else {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
syncLim = int32(syncLimParsed)
|
syncLim = int32(syncLimParsed)
|
||||||
|
if syncLim < 0 {
|
||||||
|
syncLimEnabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +139,8 @@ func NewServer(
|
|||||||
|
|
||||||
loginFilter: newLoginFilter(),
|
loginFilter: newLoginFilter(),
|
||||||
|
|
||||||
syncLim: syncLim,
|
syncLim: syncLim,
|
||||||
|
syncLimEnabled: syncLimEnabled,
|
||||||
}, nil
|
}, 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
|
// 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)
|
// 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 {
|
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")
|
return status.Errorf(codes.ResourceExhausted, "too many concurrent sync requests, please try again later")
|
||||||
}
|
}
|
||||||
s.syncSem.Add(1)
|
s.syncSem.Add(1)
|
||||||
@@ -305,7 +311,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
log.WithContext(ctx).Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
||||||
s.syncSem.Add(-1)
|
s.syncSem.Add(-1)
|
||||||
s.cancelPeerRoutines(ctx, accountID, peer)
|
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +319,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Debugf("error while notify peer connected for %s: %v", peerKey.String(), err)
|
log.WithContext(ctx).Debugf("error while notify peer connected for %s: %v", peerKey.String(), err)
|
||||||
s.syncSem.Add(-1)
|
s.syncSem.Add(-1)
|
||||||
s.cancelPeerRoutines(ctx, accountID, peer)
|
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +490,10 @@ func (s *Server) cancelPeerRoutines(ctx context.Context, accountID string, peer
|
|||||||
unlock := s.acquirePeerLockByUID(ctx, peer.Key)
|
unlock := s.acquirePeerLockByUID(ctx, peer.Key)
|
||||||
defer unlock()
|
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)
|
err := s.accountManager.OnPeerDisconnected(ctx, accountID, peer.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to disconnect peer %s properly: %v", peer.Key, err)
|
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
|
// GetWGKey returns WireGuard private key used to generate peer keys
|
||||||
func (m *TimeBasedAuthSecretsManager) GetWGKey() (wgtypes.Key, error) {
|
func (m *TimeBasedAuthSecretsManager) GetWGKey() (wgtypes.Key, error) {
|
||||||
|
log.Debug("returning wg key from secrets manager")
|
||||||
return m.wgKey, nil
|
return m.wgKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
|
||||||
"github.com/netbirdio/netbird/formatter/hook"
|
"github.com/netbirdio/netbird/formatter/hook"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
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/types"
|
||||||
"github.com/netbirdio/netbird/management/server/util"
|
"github.com/netbirdio/netbird/management/server/util"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
|
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -795,6 +795,19 @@ func IsEmbeddedIdp(i idp.Manager) bool {
|
|||||||
return ok
|
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
|
// addAccountIDToIDPAppMeta update user's app metadata in idp manager
|
||||||
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
|
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
|
||||||
if !isNil(am.idpManager) && !IsEmbeddedIdp(am.idpManager) {
|
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)
|
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)
|
embeddedIdP, embeddedIdpEnabled := idpManager.(*idpmanager.EmbeddedIdPManager)
|
||||||
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
|
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
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)
|
peers.AddEndpoints(accountManager, router, networkMapController)
|
||||||
users.AddEndpoints(accountManager, router)
|
users.AddEndpoints(accountManager, router)
|
||||||
users.AddInvitesEndpoints(accountManager, router)
|
users.AddInvitesEndpoints(accountManager, router)
|
||||||
|
|||||||
@@ -36,24 +36,22 @@ const (
|
|||||||
|
|
||||||
// handler is a handler that handles the server.Account HTTP endpoints
|
// handler is a handler that handles the server.Account HTTP endpoints
|
||||||
type handler struct {
|
type handler struct {
|
||||||
accountManager account.Manager
|
accountManager account.Manager
|
||||||
settingsManager settings.Manager
|
settingsManager settings.Manager
|
||||||
embeddedIdpEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, embeddedIdpEnabled bool, router *mux.Router) {
|
func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, router *mux.Router) {
|
||||||
accountsHandler := newHandler(accountManager, settingsManager, embeddedIdpEnabled)
|
accountsHandler := newHandler(accountManager, settingsManager)
|
||||||
router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
|
router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
|
||||||
router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
|
router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
|
||||||
router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
|
router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHandler creates a new handler HTTP handler
|
// 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{
|
return &handler{
|
||||||
accountManager: accountManager,
|
accountManager: accountManager,
|
||||||
settingsManager: settingsManager,
|
settingsManager: settingsManager,
|
||||||
embeddedIdpEnabled: embeddedIdpEnabled,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +163,7 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := toAccountResponse(accountID, settings, meta, onboarding, h.embeddedIdpEnabled)
|
resp := toAccountResponse(accountID, settings, meta, onboarding)
|
||||||
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
|
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +290,7 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding, h.embeddedIdpEnabled)
|
resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding)
|
||||||
|
|
||||||
util.WriteJSONObject(r.Context(), w, &resp)
|
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{})
|
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
|
jwtAllowGroups := settings.JWTAllowGroups
|
||||||
if jwtAllowGroups == nil {
|
if jwtAllowGroups == nil {
|
||||||
jwtAllowGroups = []string{}
|
jwtAllowGroups = []string{}
|
||||||
@@ -341,7 +339,8 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
|||||||
LazyConnectionEnabled: &settings.LazyConnectionEnabled,
|
LazyConnectionEnabled: &settings.LazyConnectionEnabled,
|
||||||
DnsDomain: &settings.DNSDomain,
|
DnsDomain: &settings.DNSDomain,
|
||||||
AutoUpdateVersion: &settings.AutoUpdateVersion,
|
AutoUpdateVersion: &settings.AutoUpdateVersion,
|
||||||
EmbeddedIdpEnabled: &embeddedIdpEnabled,
|
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
||||||
|
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.NetworkRange.IsValid() {
|
if settings.NetworkRange.IsValid() {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler {
|
|||||||
AnyTimes()
|
AnyTimes()
|
||||||
|
|
||||||
return &handler{
|
return &handler{
|
||||||
embeddedIdpEnabled: false,
|
|
||||||
accountManager: &mock_server.MockAccountManager{
|
accountManager: &mock_server.MockAccountManager{
|
||||||
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
||||||
return account.Settings, nil
|
return account.Settings, nil
|
||||||
@@ -124,6 +123,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: true,
|
expectedArray: true,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -148,6 +148,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -172,6 +173,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr("latest"),
|
AutoUpdateVersion: sr("latest"),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -196,6 +198,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -220,6 +223,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -244,6 +248,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
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)
|
util.WriteErrorResponse("failed to check instance status", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.WithContext(r.Context()).Infof("instance setup status: %v", setupRequired)
|
||||||
util.WriteJSONObject(r.Context(), w, api.InstanceStatus{
|
util.WriteJSONObject(r.Context(), w, api.InstanceStatus{
|
||||||
SetupRequired: setupRequired,
|
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")
|
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",
|
name: "invalid JSON",
|
||||||
requestBody: `{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")
|
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",
|
name: "missing token",
|
||||||
token: "",
|
token: "",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
|||||||
proxyController := integrations.NewController(store)
|
proxyController := integrations.NewController(store)
|
||||||
userManager := users.NewManager(store)
|
userManager := users.NewManager(store)
|
||||||
permissionsManager := permissions.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)
|
peersManager := peers.NewManager(store, permissionsManager)
|
||||||
|
|
||||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ type EmbeddedIdPConfig struct {
|
|||||||
Owner *OwnerConfig
|
Owner *OwnerConfig
|
||||||
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
|
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
|
||||||
SignKeyRefreshEnabled bool
|
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.
|
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
|
||||||
@@ -105,6 +110,8 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
|||||||
Issuer: "NetBird",
|
Issuer: "NetBird",
|
||||||
Theme: "light",
|
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,
|
EnablePasswordDB: true,
|
||||||
StaticClients: []storage.Client{
|
StaticClients: []storage.Client{
|
||||||
{
|
{
|
||||||
@@ -192,11 +199,32 @@ func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v", config)
|
||||||
|
|
||||||
provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
|
provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create embedded IdP provider: %w", err)
|
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)
|
log.WithContext(ctx).Infof("embedded Dex IDP initialized with issuer: %s", yamlConfig.Issuer)
|
||||||
|
|
||||||
return &EmbeddedIdPManager{
|
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)
|
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)
|
indexedUsers := make(map[string][]*UserData)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], &UserData{
|
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
|
return indexedUsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in the embedded IdP.
|
// CreateUser creates a new user in the embedded IdP.
|
||||||
func (m *EmbeddedIdPManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
|
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 {
|
if m.appMetrics != nil {
|
||||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
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.
|
// 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.
|
// 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) {
|
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 {
|
if m.appMetrics != nil {
|
||||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
m.appMetrics.IDPMetrics().CountCreateUser()
|
||||||
}
|
}
|
||||||
@@ -553,3 +593,13 @@ func (m *EmbeddedIdPManager) GetClientIDs() []string {
|
|||||||
func (m *EmbeddedIdPManager) GetUserIDClaim() string {
|
func (m *EmbeddedIdPManager) GetUserIDClaim() string {
|
||||||
return defaultUserIDClaim
|
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 {
|
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)
|
users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
hasLocalUsers := len(users) > 0
|
||||||
|
|
||||||
m.setupMu.Lock()
|
m.setupMu.Lock()
|
||||||
m.setupRequired = len(users) == 0
|
m.setupRequired = !(hasAccounts || hasLocalUsers)
|
||||||
m.setupMu.Unlock()
|
m.setupMu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -24,19 +24,28 @@ type Manager interface {
|
|||||||
UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error)
|
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 {
|
type managerImpl struct {
|
||||||
store store.Store
|
store store.Store
|
||||||
extraSettingsManager extra_settings.Manager
|
extraSettingsManager extra_settings.Manager
|
||||||
userManager users.Manager
|
userManager users.Manager
|
||||||
permissionsManager permissions.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{
|
return &managerImpl{
|
||||||
store: store,
|
store: store,
|
||||||
extraSettingsManager: extraSettingsManager,
|
extraSettingsManager: extraSettingsManager,
|
||||||
userManager: userManager,
|
userManager: userManager,
|
||||||
permissionsManager: permissionsManager,
|
permissionsManager: permissionsManager,
|
||||||
|
idpConfig: idpConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +83,10 @@ func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string)
|
|||||||
settings.Extra.FlowDnsCollectionEnabled = extraSettings.FlowDnsCollectionEnabled
|
settings.Extra.FlowDnsCollectionEnabled = extraSettings.FlowDnsCollectionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in IdP-related runtime settings
|
||||||
|
settings.EmbeddedIdpEnabled = m.idpConfig.EmbeddedIdpEnabled
|
||||||
|
settings.LocalAuthDisabled = m.idpConfig.LocalAuthDisabled
|
||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ type Settings struct {
|
|||||||
|
|
||||||
// AutoUpdateVersion client auto-update version
|
// AutoUpdateVersion client auto-update version
|
||||||
AutoUpdateVersion string `gorm:"default:'disabled'"`
|
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
|
// Copy copies the Settings struct
|
||||||
@@ -76,6 +84,8 @@ func (s *Settings) Copy() *Settings {
|
|||||||
DNSDomain: s.DNSDomain,
|
DNSDomain: s.DNSDomain,
|
||||||
NetworkRange: s.NetworkRange,
|
NetworkRange: s.NetworkRange,
|
||||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||||
|
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
||||||
|
LocalAuthDisabled: s.LocalAuthDisabled,
|
||||||
}
|
}
|
||||||
if s.Extra != nil {
|
if s.Extra != nil {
|
||||||
settings.Extra = s.Extra.Copy()
|
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
|
// 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.
|
// 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) {
|
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)
|
inviter, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, inviterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get inviter user: %w", err)
|
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")
|
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 {
|
if err := validateUserInvite(invite); err != nil {
|
||||||
return nil, err
|
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")
|
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 == "" {
|
if password == "" {
|
||||||
return status.Errorf(status.InvalidArgument, "password is required")
|
return status.Errorf(status.InvalidArgument, "password is required")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,11 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
readOnly: true
|
readOnly: true
|
||||||
example: false
|
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:
|
required:
|
||||||
- peer_login_expiration_enabled
|
- peer_login_expiration_enabled
|
||||||
- peer_login_expiration
|
- peer_login_expiration
|
||||||
|
|||||||
@@ -415,6 +415,9 @@ type AccountSettings struct {
|
|||||||
// LazyConnectionEnabled Enables or disables experimental lazy connection
|
// LazyConnectionEnabled Enables or disables experimental lazy connection
|
||||||
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
|
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 Allows to define a custom network range for the account in CIDR format
|
||||||
NetworkRange *string `json:"network_range,omitempty"`
|
NetworkRange *string `json:"network_range,omitempty"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user