From cffb712ad17208fe33367d515fb8abadc825bf25 Mon Sep 17 00:00:00 2001 From: braginini Date: Sat, 31 Jan 2026 14:30:14 +0100 Subject: [PATCH 1/8] Disable local users for a smooth single-idp mode --- idp/dex/connector.go | 66 ++++++ management/server/idp/embedded.go | 49 +++++ management/server/idp/embedded_test.go | 269 +++++++++++++++++++++++++ 3 files changed, 384 insertions(+) diff --git a/idp/dex/connector.go b/idp/dex/connector.go index cad68214133..26e610d90fb 100644 --- a/idp/dex/connector.go +++ b/idp/dex/connector.go @@ -327,6 +327,72 @@ 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 +} + +// IsLocalAuthEnabled checks if the local (password) connector is currently enabled. +func (p *Provider) IsLocalAuthEnabled(ctx context.Context) (bool, error) { + _, err := p.storage.GetConnector(ctx, "local") + if err == nil { + return true, nil + } + if errors.Is(err, storage.ErrNotFound) { + return false, nil + } + return false, fmt.Errorf("failed to check local connector: %w", err) +} + +// 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 { diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index db7a91fa362..fa3e6956a83 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -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{ { @@ -197,6 +204,25 @@ func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMe 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{ @@ -553,3 +579,26 @@ func (m *EmbeddedIdPManager) GetClientIDs() []string { func (m *EmbeddedIdPManager) GetUserIDClaim() string { return defaultUserIDClaim } + +// IsLocalAuthEnabled checks if the local (password) authentication is currently enabled. +func (m *EmbeddedIdPManager) IsLocalAuthEnabled(ctx context.Context) (bool, error) { + return m.provider.IsLocalAuthEnabled(ctx) +} + +// 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) +} + +// DisableLocalAuth disables local (email/password) authentication. +// Returns an error if no other identity providers are configured. +// Existing local users are preserved and will be able to login again if re-enabled. +func (m *EmbeddedIdPManager) DisableLocalAuth(ctx context.Context) error { + return m.provider.DisableLocalAuth(ctx) +} + +// EnableLocalAuth enables local (email/password) authentication. +// Existing local users will be able to login again. +func (m *EmbeddedIdPManager) EnableLocalAuth(ctx context.Context) error { + return m.provider.EnableLocalAuth(ctx) +} diff --git a/management/server/idp/embedded_test.go b/management/server/idp/embedded_test.go index d8d3009dd9d..591540c5427 100644 --- a/management/server/idp/embedded_test.go +++ b/management/server/idp/embedded_test.go @@ -370,3 +370,272 @@ func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) { }) } } + +func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { + ctx := context.Background() + + t.Run("cannot disable local auth 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("disable local auth at runtime without other connectors fails", 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) }() + + // Try to disable local auth - should fail + err = manager.DisableLocalAuth(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "no other identity providers configured") + + // Verify local auth is still enabled + enabled, err := manager.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.True(t, enabled) + }) + + t.Run("disable and re-enable local auth preserves users", 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) }() + + // Create a user + userData, err := manager.CreateUser(ctx, "test@example.com", "Test User", "account1", "admin@example.com") + require.NoError(t, err) + userID := userData.ID + + // Add an external connector so we can disable local auth + _, err = manager.CreateConnector(ctx, &dex.ConnectorConfig{ + ID: "google-test", + Name: "Google Test", + Type: "google", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + }) + require.NoError(t, err) + + // Verify we have a non-local connector + hasOthers, err := manager.HasNonLocalConnectors(ctx) + require.NoError(t, err) + assert.True(t, hasOthers) + + // Disable local auth + err = manager.DisableLocalAuth(ctx) + require.NoError(t, err) + + // Verify local auth is disabled + enabled, err := manager.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.False(t, enabled) + + // Re-enable local auth + err = manager.EnableLocalAuth(ctx) + require.NoError(t, err) + + // Verify local auth is enabled again + enabled, err = manager.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.True(t, enabled) + + // Verify the user still exists + lookedUp, err := manager.GetUserDataByID(ctx, userID, AppMetadata{}) + require.NoError(t, err) + assert.Equal(t, "test@example.com", lookedUp.Email) + }) + + 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 + enabled, err := manager2.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.False(t, enabled) + + // 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("disabling already disabled local auth is idempotent", 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) }() + + // Add an external connector + _, err = manager.CreateConnector(ctx, &dex.ConnectorConfig{ + ID: "google-test", + Name: "Google Test", + Type: "google", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + }) + require.NoError(t, err) + + // Disable local auth + err = manager.DisableLocalAuth(ctx) + require.NoError(t, err) + + // Disable again - should not error + err = manager.DisableLocalAuth(ctx) + require.NoError(t, err) + + // Verify still disabled + enabled, err := manager.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.False(t, enabled) + }) + + t.Run("enabling already enabled local auth is idempotent", 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 + enabled, err := manager.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.True(t, enabled) + + // Enable again - should not error + err = manager.EnableLocalAuth(ctx) + require.NoError(t, err) + + // Verify still enabled + enabled, err = manager.IsLocalAuthEnabled(ctx) + require.NoError(t, err) + assert.True(t, enabled) + }) +} From cdcdcffa5099e8787e3432372b2f94c4f3e477b7 Mon Sep 17 00:00:00 2001 From: braginini Date: Sat, 31 Jan 2026 17:10:24 +0100 Subject: [PATCH 2/8] Expose local_auth_disabled property in AccountSettings --- idp/dex/connector.go | 12 ------ management/internals/server/modules.go | 9 ++++- management/server/account.go | 15 ++++++- management/server/http/handler.go | 4 +- .../handlers/accounts/accounts_handler.go | 25 ++++++------ .../accounts/accounts_handler_test.go | 7 +++- .../testing/testing_tools/channel/channel.go | 2 +- management/server/idp/embedded.go | 6 +-- management/server/idp/embedded_test.go | 39 ++++--------------- management/server/settings/manager.go | 15 ++++++- management/server/types/settings.go | 10 +++++ management/server/user.go | 12 ++++++ shared/management/http/api/openapi.yml | 5 +++ shared/management/http/api/types.gen.go | 3 ++ 14 files changed, 98 insertions(+), 66 deletions(-) diff --git a/idp/dex/connector.go b/idp/dex/connector.go index 26e610d90fb..ba2bb1f000f 100644 --- a/idp/dex/connector.go +++ b/idp/dex/connector.go @@ -346,18 +346,6 @@ func (p *Provider) HasNonLocalConnectors(ctx context.Context) (bool, error) { return false, nil } -// IsLocalAuthEnabled checks if the local (password) connector is currently enabled. -func (p *Provider) IsLocalAuthEnabled(ctx context.Context) (bool, error) { - _, err := p.storage.GetConnector(ctx, "local") - if err == nil { - return true, nil - } - if errors.Is(err, storage.ErrNotFound) { - return false, nil - } - return false, fmt.Errorf("failed to check local connector: %w", err) -} - // DisableLocalAuth removes the local (password) connector. // Returns an error if no other connectors are configured. func (p *Provider) DisableLocalAuth(ctx context.Context) error { diff --git a/management/internals/server/modules.go b/management/internals/server/modules.go index b51e2ebb2b4..31badf9d0c5 100644 --- a/management/internals/server/modules.go +++ b/management/internals/server/modules.go @@ -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) }) } diff --git a/management/server/account.go b/management/server/account.go index ba5f0cffad2..8f9dad031c9 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -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) { diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 32a97ff44a9..79431a0a333 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -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) diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index de778d59acc..122c061ce24 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -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() { diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index e455372c89a..6cbd5908d92 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -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, diff --git a/management/server/http/testing/testing_tools/channel/channel.go b/management/server/http/testing/testing_tools/channel/channel.go index 9339c3541fb..1fd4c9bad36 100644 --- a/management/server/http/testing/testing_tools/channel/channel.go +++ b/management/server/http/testing/testing_tools/channel/channel.go @@ -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) diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index fa3e6956a83..51b2478c945 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -580,9 +580,9 @@ func (m *EmbeddedIdPManager) GetUserIDClaim() string { return defaultUserIDClaim } -// IsLocalAuthEnabled checks if the local (password) authentication is currently enabled. -func (m *EmbeddedIdPManager) IsLocalAuthEnabled(ctx context.Context) (bool, error) { - return m.provider.IsLocalAuthEnabled(ctx) +// 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. diff --git a/management/server/idp/embedded_test.go b/management/server/idp/embedded_test.go index 591540c5427..2399efcf060 100644 --- a/management/server/idp/embedded_test.go +++ b/management/server/idp/embedded_test.go @@ -421,10 +421,8 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "no other identity providers configured") - // Verify local auth is still enabled - enabled, err := manager.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.True(t, enabled) + // Verify local auth is still enabled (config unchanged) + assert.False(t, manager.IsLocalAuthDisabled()) }) t.Run("disable and re-enable local auth preserves users", func(t *testing.T) { @@ -471,20 +469,10 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { err = manager.DisableLocalAuth(ctx) require.NoError(t, err) - // Verify local auth is disabled - enabled, err := manager.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.False(t, enabled) - // Re-enable local auth err = manager.EnableLocalAuth(ctx) require.NoError(t, err) - // Verify local auth is enabled again - enabled, err = manager.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.True(t, enabled) - // Verify the user still exists lookedUp, err := manager.GetUserDataByID(ctx, userID, AppMetadata{}) require.NoError(t, err) @@ -549,10 +537,8 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { require.NoError(t, err) defer func() { _ = manager2.Stop(ctx) }() - // Verify local auth is disabled - enabled, err := manager2.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.False(t, enabled) + // 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{}) @@ -597,11 +583,6 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { // Disable again - should not error err = manager.DisableLocalAuth(ctx) require.NoError(t, err) - - // Verify still disabled - enabled, err := manager.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.False(t, enabled) }) t.Run("enabling already enabled local auth is idempotent", func(t *testing.T) { @@ -624,18 +605,14 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { require.NoError(t, err) defer func() { _ = manager.Stop(ctx) }() - // Verify local auth is enabled by default - enabled, err := manager.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.True(t, enabled) + // Verify local auth is enabled by default (config) + assert.False(t, manager.IsLocalAuthDisabled()) // Enable again - should not error err = manager.EnableLocalAuth(ctx) require.NoError(t, err) - // Verify still enabled - enabled, err = manager.IsLocalAuthEnabled(ctx) - require.NoError(t, err) - assert.True(t, enabled) + // Verify still enabled (config unchanged) + assert.False(t, manager.IsLocalAuthDisabled()) }) } diff --git a/management/server/settings/manager.go b/management/server/settings/manager.go index 2b289657281..74af0a3ef4e 100644 --- a/management/server/settings/manager.go +++ b/management/server/settings/manager.go @@ -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 } diff --git a/management/server/types/settings.go b/management/server/types/settings.go index 867e12befba..a94e01b7869 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -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() diff --git a/management/server/user.go b/management/server/user.go index 51da7a633c6..48005f325aa 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -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") } diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 26d2387d15d..b9a8eae3aa5 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -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 diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index e8c044b322f..fd7c6191757 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -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"` From 7e324a7eb7ddefa5bfeab9800b5f8f14c60893d7 Mon Sep 17 00:00:00 2001 From: braginini Date: Sat, 31 Jan 2026 17:22:35 +0100 Subject: [PATCH 3/8] Remove unused methods --- management/server/idp/embedded.go | 13 --- management/server/idp/embedded_test.go | 135 +------------------------ 2 files changed, 3 insertions(+), 145 deletions(-) diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index 51b2478c945..18e30c62053 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -589,16 +589,3 @@ func (m *EmbeddedIdPManager) IsLocalAuthDisabled() bool { func (m *EmbeddedIdPManager) HasNonLocalConnectors(ctx context.Context) (bool, error) { return m.provider.HasNonLocalConnectors(ctx) } - -// DisableLocalAuth disables local (email/password) authentication. -// Returns an error if no other identity providers are configured. -// Existing local users are preserved and will be able to login again if re-enabled. -func (m *EmbeddedIdPManager) DisableLocalAuth(ctx context.Context) error { - return m.provider.DisableLocalAuth(ctx) -} - -// EnableLocalAuth enables local (email/password) authentication. -// Existing local users will be able to login again. -func (m *EmbeddedIdPManager) EnableLocalAuth(ctx context.Context) error { - return m.provider.EnableLocalAuth(ctx) -} diff --git a/management/server/idp/embedded_test.go b/management/server/idp/embedded_test.go index 2399efcf060..17aa486f397 100644 --- a/management/server/idp/embedded_test.go +++ b/management/server/idp/embedded_test.go @@ -374,7 +374,7 @@ func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) { func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { ctx := context.Background() - t.Run("cannot disable local auth without other connectors", func(t *testing.T) { + 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) @@ -396,7 +396,7 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { assert.Contains(t, err.Error(), "no other identity providers configured") }) - t.Run("disable local auth at runtime without other connectors fails", func(t *testing.T) { + 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) @@ -416,69 +416,10 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { require.NoError(t, err) defer func() { _ = manager.Stop(ctx) }() - // Try to disable local auth - should fail - err = manager.DisableLocalAuth(ctx) - require.Error(t, err) - assert.Contains(t, err.Error(), "no other identity providers configured") - - // Verify local auth is still enabled (config unchanged) + // Verify local auth is enabled by default assert.False(t, manager.IsLocalAuthDisabled()) }) - t.Run("disable and re-enable local auth preserves users", 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) }() - - // Create a user - userData, err := manager.CreateUser(ctx, "test@example.com", "Test User", "account1", "admin@example.com") - require.NoError(t, err) - userID := userData.ID - - // Add an external connector so we can disable local auth - _, err = manager.CreateConnector(ctx, &dex.ConnectorConfig{ - ID: "google-test", - Name: "Google Test", - Type: "google", - ClientID: "test-client-id", - ClientSecret: "test-client-secret", - }) - require.NoError(t, err) - - // Verify we have a non-local connector - hasOthers, err := manager.HasNonLocalConnectors(ctx) - require.NoError(t, err) - assert.True(t, hasOthers) - - // Disable local auth - err = manager.DisableLocalAuth(ctx) - require.NoError(t, err) - - // Re-enable local auth - err = manager.EnableLocalAuth(ctx) - require.NoError(t, err) - - // Verify the user still exists - lookedUp, err := manager.GetUserDataByID(ctx, userID, AppMetadata{}) - require.NoError(t, err) - assert.Equal(t, "test@example.com", lookedUp.Email) - }) - 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) @@ -545,74 +486,4 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { require.NoError(t, err) assert.Equal(t, "preserved@example.com", lookedUp.Email) }) - - t.Run("disabling already disabled local auth is idempotent", 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) }() - - // Add an external connector - _, err = manager.CreateConnector(ctx, &dex.ConnectorConfig{ - ID: "google-test", - Name: "Google Test", - Type: "google", - ClientID: "test-client-id", - ClientSecret: "test-client-secret", - }) - require.NoError(t, err) - - // Disable local auth - err = manager.DisableLocalAuth(ctx) - require.NoError(t, err) - - // Disable again - should not error - err = manager.DisableLocalAuth(ctx) - require.NoError(t, err) - }) - - t.Run("enabling already enabled local auth is idempotent", 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 (config) - assert.False(t, manager.IsLocalAuthDisabled()) - - // Enable again - should not error - err = manager.EnableLocalAuth(ctx) - require.NoError(t, err) - - // Verify still enabled (config unchanged) - assert.False(t, manager.IsLocalAuthDisabled()) - }) } From bbb189cfd777c579e3b5a72976e4e01621d8a27e Mon Sep 17 00:00:00 2001 From: braginini Date: Sat, 31 Jan 2026 20:21:17 +0100 Subject: [PATCH 4/8] Add more tests --- .../handlers/users/invites_handler_test.go | 17 +++ management/server/idp/embedded.go | 8 ++ management/server/idp/embedded_test.go | 114 ++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/management/server/http/handlers/users/invites_handler_test.go b/management/server/http/handlers/users/invites_handler_test.go index 80826b9d47b..529ea24d649 100644 --- a/management/server/http/handlers/users/invites_handler_test.go +++ b/management/server/http/handlers/users/invites_handler_test.go @@ -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: "", diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index 18e30c62053..177fcf7c3cf 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -321,6 +321,10 @@ func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]* // 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() } @@ -390,6 +394,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() } diff --git a/management/server/idp/embedded_test.go b/management/server/idp/embedded_test.go index 17aa486f397..4dda483fbc1 100644 --- a/management/server/idp/embedded_test.go +++ b/management/server/idp/embedded_test.go @@ -486,4 +486,118 @@ func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) { 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") + }) } From 02901338dbd435c1bf26990536d5813fec8a3c8b Mon Sep 17 00:00:00 2001 From: braginini Date: Sun, 1 Feb 2026 11:46:35 +0100 Subject: [PATCH 5/8] Add more debug logs --- management/server/http/handlers/instance/instance_handler.go | 2 +- management/server/idp/embedded.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/management/server/http/handlers/instance/instance_handler.go b/management/server/http/handlers/instance/instance_handler.go index 5d8baaf8dc1..a8e332f0864 100644 --- a/management/server/http/handlers/instance/instance_handler.go +++ b/management/server/http/handlers/instance/instance_handler.go @@ -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, }) diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index 177fcf7c3cf..79e71aa8d30 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -199,6 +199,8 @@ func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMe return nil, err } + log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v") + provider, err := dex.NewProviderFromYAML(ctx, yamlConfig) if err != nil { return nil, fmt.Errorf("failed to create embedded IdP provider: %w", err) @@ -316,6 +318,8 @@ 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 } From e28c0871a1057be600d2d0a5c4f9f64bfb3517cd Mon Sep 17 00:00:00 2001 From: braginini Date: Sun, 1 Feb 2026 11:48:11 +0100 Subject: [PATCH 6/8] Add more debug logs --- management/server/idp/embedded.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index 79e71aa8d30..09d38fb0159 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -199,7 +199,7 @@ func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMe return nil, err } - log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v") + log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v", config) provider, err := dex.NewProviderFromYAML(ctx, yamlConfig) if err != nil { From 0c38d9ec075ea7e4fb3a0e5b625430316e5ab9f0 Mon Sep 17 00:00:00 2001 From: braginini Date: Sun, 1 Feb 2026 12:39:30 +0100 Subject: [PATCH 7/8] Improve setup required check --- management/server/idp/embedded.go | 2 ++ management/server/instance/manager.go | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index 09d38fb0159..a27050a2658 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -309,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{ diff --git a/management/server/instance/manager.go b/management/server/instance/manager.go index 6a0509ebde0..19e3abdc01c 100644 --- a/management/server/instance/manager.go +++ b/management/server/instance/manager.go @@ -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 From b3749c09002b035ac9ca0a52eebb9550993c65c3 Mon Sep 17 00:00:00 2001 From: braginini Date: Sun, 1 Feb 2026 12:58:28 +0100 Subject: [PATCH 8/8] Format logs --- management/server/http/handlers/instance/instance_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/http/handlers/instance/instance_handler.go b/management/server/http/handlers/instance/instance_handler.go index a8e332f0864..cd9fae6b83f 100644 --- a/management/server/http/handlers/instance/instance_handler.go +++ b/management/server/http/handlers/instance/instance_handler.go @@ -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) + log.WithContext(r.Context()).Infof("instance setup status: %v", setupRequired) util.WriteJSONObject(r.Context(), w, api.InstanceStatus{ SetupRequired: setupRequired, })