mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-18 14:49:57 +00:00
[management] Enable MFA for local users (#5804)
* wip: totp for local users * fix providers not getting populated * polished UI and fix post_login_redirect_uri * fix: make sure logout is only prompted from oidc flow Signed-off-by: jnfrati <nicofrati@gmail.com> * update templates Signed-off-by: jnfrati <nicofrati@gmail.com> * deps: update dex dependency Signed-off-by: jnfrati <nicofrati@gmail.com> * fix qube issues Signed-off-by: jnfrati <nicofrati@gmail.com> * replace window with globalThis on home html Signed-off-by: jnfrati <nicofrati@gmail.com> * fixed coderabbit comments Signed-off-by: jnfrati <nicofrati@gmail.com> * debug * remove unused config and rename totp issuer * deps: update dex reference to latest * add dashboard post logout redirect uri to embedded config * implemented api for mfa configuration * update docs and config parsing * catch error on idp manager init mfa * fix tests * Add remember me for MFA * Add cookie encryption and session share between tabs * fixed logout showing non actionable error and session cookie encription key * fixed missing mfa settings on sql query for account * fix code index for mfa activity --------- Signed-off-by: jnfrati <nicofrati@gmail.com> Co-authored-by: braginini <bangvalo@gmail.com>
This commit is contained in:
@@ -386,6 +386,9 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
||||
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = am.handleLocalMfaSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oldSettings.DNSDomain != newSettings.DNSDomain {
|
||||
eventMeta := map[string]any{
|
||||
"old_dns_domain": oldSettings.DNSDomain,
|
||||
@@ -602,6 +605,29 @@ func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) handleLocalMfaSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error {
|
||||
if oldSettings.LocalMfaEnabled == newSettings.LocalMfaEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
embeddedIdp, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := embeddedIdp.SetMFAEnabled(ctx, newSettings.LocalMfaEnabled); err != nil {
|
||||
return fmt.Errorf("failed to toggle MFA: %w", err)
|
||||
}
|
||||
|
||||
if newSettings.LocalMfaEnabled {
|
||||
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLocalMfaEnabled, nil)
|
||||
} else {
|
||||
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLocalMfaDisabled, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) peerLoginExpirationJob(ctx context.Context, accountID string) func() (time.Duration, bool) {
|
||||
return func() (time.Duration, bool) {
|
||||
//nolint
|
||||
|
||||
@@ -236,6 +236,11 @@ const (
|
||||
// AccountIPv6Disabled indicates that a user disabled IPv6 overlay for the account
|
||||
AccountIPv6Disabled Activity = 122
|
||||
|
||||
// AccountLocalMfaEnabled indicates that a user enabled TOTP MFA for local users
|
||||
AccountLocalMfaEnabled Activity = 123
|
||||
// AccountLocalMfaDisabled indicates that a user disabled TOTP MFA for local users
|
||||
AccountLocalMfaDisabled Activity = 124
|
||||
|
||||
AccountDeleted Activity = 99999
|
||||
)
|
||||
|
||||
@@ -386,6 +391,9 @@ var activityMap = map[Activity]Code{
|
||||
AccountPeerExposeEnabled: {"Account peer expose enabled", "account.setting.peer.expose.enable"},
|
||||
AccountPeerExposeDisabled: {"Account peer expose disabled", "account.setting.peer.expose.disable"},
|
||||
|
||||
AccountLocalMfaEnabled: {"Account local MFA enabled", "account.setting.local.mfa.enable"},
|
||||
AccountLocalMfaDisabled: {"Account local MFA disabled", "account.setting.local.mfa.disable"},
|
||||
|
||||
DomainAdded: {"Domain added", "domain.add"},
|
||||
DomainDeleted: {"Domain deleted", "domain.delete"},
|
||||
DomainValidated: {"Domain validated", "domain.validate"},
|
||||
|
||||
@@ -277,6 +277,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
|
||||
if req.Settings.AutoUpdateAlways != nil {
|
||||
returnSettings.AutoUpdateAlways = *req.Settings.AutoUpdateAlways
|
||||
}
|
||||
if req.Settings.LocalMfaEnabled != nil {
|
||||
returnSettings.LocalMfaEnabled = *req.Settings.LocalMfaEnabled
|
||||
}
|
||||
if req.Settings.Ipv6EnabledGroups != nil {
|
||||
returnSettings.IPv6EnabledGroups = *req.Settings.Ipv6EnabledGroups
|
||||
}
|
||||
@@ -412,6 +415,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
||||
Ipv6EnabledGroups: &settings.IPv6EnabledGroups,
|
||||
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
||||
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
||||
LocalMfaEnabled: &settings.LocalMfaEnabled,
|
||||
}
|
||||
|
||||
if settings.NetworkRange.IsValid() {
|
||||
|
||||
@@ -131,6 +131,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: true,
|
||||
expectedID: accountID,
|
||||
@@ -157,6 +158,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -183,6 +185,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
AutoUpdateVersion: sr("latest"),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -209,6 +212,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -235,6 +239,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -261,6 +266,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
AutoUpdateVersion: sr(""),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
|
||||
@@ -2,9 +2,11 @@ package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
@@ -17,12 +19,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
staticClientDashboard = "netbird-dashboard"
|
||||
staticClientCLI = "netbird-cli"
|
||||
defaultCLIRedirectURL1 = "http://localhost:53000/"
|
||||
defaultCLIRedirectURL2 = "http://localhost:54000/"
|
||||
defaultScopes = "openid profile email groups"
|
||||
defaultUserIDClaim = "sub"
|
||||
staticClientDashboard = "netbird-dashboard"
|
||||
staticClientCLI = "netbird-cli"
|
||||
defaultCLIRedirectURL1 = "http://localhost:53000/"
|
||||
defaultCLIRedirectURL2 = "http://localhost:54000/"
|
||||
defaultScopes = "openid profile email groups"
|
||||
defaultUserIDClaim = "sub"
|
||||
sessionCookieEncryptionKeyEnv = "NB_IDP_SESSION_COOKIE_ENCRYPTION_KEY"
|
||||
)
|
||||
|
||||
// EmbeddedIdPConfig contains configuration for the embedded Dex OIDC identity provider
|
||||
@@ -49,6 +52,26 @@ type EmbeddedIdPConfig struct {
|
||||
// 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
|
||||
// MfaSessionMaxLifetime is the maximum MFA session duration from creation (e.g. "24h").
|
||||
// Defaults to "24h" if empty.
|
||||
MfaSessionMaxLifetime string
|
||||
// MfaSessionIdleTimeout is the idle timeout after which the MFA session expires (e.g. "1h").
|
||||
// Defaults to "1h" if empty.
|
||||
MfaSessionIdleTimeout string
|
||||
// MfaSessionRememberMe controls the default state of the "remember me" checkbox on the
|
||||
// login screen. When true, the session cookie persists across browser tabs/restarts so
|
||||
// MFA is not re-prompted until the session expires. Defaults to false.
|
||||
MfaSessionRememberMe bool
|
||||
// SessionCookieEncryptionKey is the optional AES key used to encrypt embedded IdP session cookies.
|
||||
// It can also be set with NB_IDP_SESSION_COOKIE_ENCRYPTION_KEY. The value must be 16, 24, or 32
|
||||
// bytes when provided as a raw string, or base64-encoded to one of those lengths.
|
||||
SessionCookieEncryptionKey string
|
||||
// Dashboard Post logout redirect URIs, these are required to tell
|
||||
// Dex what to allow when an RP-Initiated logout is started by the frontend
|
||||
// at least one of these must match the dashboard base URL or the dashboard
|
||||
// DASHBOARD_POST_LOGOUT_URL environment variable
|
||||
// WARNING: Dex only uses exact match, not wildcards
|
||||
DashboardPostLogoutRedirectURIs []string
|
||||
// StaticConnectors are additional connectors to seed during initialization
|
||||
StaticConnectors []dex.Connector
|
||||
}
|
||||
@@ -126,6 +149,11 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
// todo: resolve import cycle
|
||||
dashboardRedirectURIs = append(dashboardRedirectURIs, baseURL+"/api/reverse-proxy/callback")
|
||||
|
||||
dashboardPostLogoutRedirectURIs := c.DashboardPostLogoutRedirectURIs
|
||||
// It is safe to assume that most installations will share the location of the
|
||||
// MGMT api and the dashboard, adding baseURL means less configuration for the instance admin
|
||||
dashboardPostLogoutRedirectURIs = append(dashboardPostLogoutRedirectURIs, baseURL)
|
||||
|
||||
cfg := &dex.YAMLConfig{
|
||||
Issuer: c.Issuer,
|
||||
Storage: dex.Storage{
|
||||
@@ -148,10 +176,11 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
EnablePasswordDB: true,
|
||||
StaticClients: []storage.Client{
|
||||
{
|
||||
ID: staticClientDashboard,
|
||||
Name: "NetBird Dashboard",
|
||||
Public: true,
|
||||
RedirectURIs: dashboardRedirectURIs,
|
||||
ID: staticClientDashboard,
|
||||
Name: "NetBird Dashboard",
|
||||
Public: true,
|
||||
RedirectURIs: dashboardRedirectURIs,
|
||||
PostLogoutRedirectURIs: sanitizePostLogoutRedirectURIs(dashboardPostLogoutRedirectURIs),
|
||||
},
|
||||
{
|
||||
ID: staticClientCLI,
|
||||
@@ -163,6 +192,12 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
StaticConnectors: c.StaticConnectors,
|
||||
}
|
||||
|
||||
// Always initialize MFA providers and sessions so TOTP can be toggled at runtime.
|
||||
// MFAChain on clients is NOT set here — it's synced from the DB setting on startup.
|
||||
if err := configureMFA(cfg, c.MfaSessionMaxLifetime, c.MfaSessionIdleTimeout, c.MfaSessionRememberMe, c.SessionCookieEncryptionKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add owner user if provided
|
||||
if c.Owner != nil && c.Owner.Email != "" && c.Owner.Hash != "" {
|
||||
username := c.Owner.Username
|
||||
@@ -182,6 +217,100 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Due to how the frontend generates the logout, sometimes it appends a trailing slash
|
||||
// and because Dex only allows exact matches, we need to make sure we always have both
|
||||
// versions of each provided uri
|
||||
func sanitizePostLogoutRedirectURIs(uris []string) []string {
|
||||
result := make([]string, 0)
|
||||
for _, uri := range uris {
|
||||
if strings.HasSuffix(uri, "/") {
|
||||
result = append(result, uri)
|
||||
result = append(result, strings.TrimSuffix(uri, "/"))
|
||||
} else {
|
||||
result = append(result, uri)
|
||||
result = append(result, uri+"/")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout string, rememberMe bool, sessionCookieEncryptionKey string) error {
|
||||
cfg.MFA.Authenticators = []dex.MFAAuthenticator{{
|
||||
ID: "default-totp",
|
||||
// Has to be caps otherwise it will fail
|
||||
Type: "TOTP",
|
||||
Config: map[string]interface{}{
|
||||
"issuer": "NetBird",
|
||||
},
|
||||
ConnectorTypes: []string{"local"},
|
||||
}}
|
||||
|
||||
if sessionMaxLifetime == "" {
|
||||
sessionMaxLifetime = "24h"
|
||||
}
|
||||
if sessionIdleTimeout == "" {
|
||||
sessionIdleTimeout = "1h"
|
||||
}
|
||||
|
||||
cookieEncryptionKey, err := resolveSessionCookieEncryptionKey(sessionCookieEncryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Sessions = &dex.Sessions{
|
||||
CookieName: "netbird-session",
|
||||
AbsoluteLifetime: sessionMaxLifetime,
|
||||
ValidIfNotUsedFor: sessionIdleTimeout,
|
||||
RememberMeCheckedByDefault: &rememberMe,
|
||||
SSOSharedWithDefault: "all",
|
||||
CookieEncryptionKey: cookieEncryptionKey,
|
||||
}
|
||||
// Absolutely required, otherwise the dex server will omit the MFA configuration entirely
|
||||
os.Setenv("DEX_SESSIONS_ENABLED", "true")
|
||||
|
||||
// Note: MFAChain on clients is NOT set here.
|
||||
// It is toggled at runtime via SetMFAEnabled() based on the account settings DB value.
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveSessionCookieEncryptionKey(configuredKey string) (string, error) {
|
||||
key := strings.TrimSpace(configuredKey)
|
||||
if key == "" {
|
||||
key = strings.TrimSpace(os.Getenv(sessionCookieEncryptionKeyEnv))
|
||||
}
|
||||
if key == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if validSessionCookieEncryptionKeyLength(len([]byte(key))) {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
for _, encoding := range []*base64.Encoding{
|
||||
base64.StdEncoding,
|
||||
base64.RawStdEncoding,
|
||||
base64.URLEncoding,
|
||||
base64.RawURLEncoding,
|
||||
} {
|
||||
decoded, err := encoding.DecodeString(key)
|
||||
if err == nil && validSessionCookieEncryptionKeyLength(len(decoded)) {
|
||||
return string(decoded), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid embedded IdP session cookie encryption key: %s (or sessionCookieEncryptionKey) must be 16, 24, or 32 bytes as a raw string or base64-encoded to one of those lengths; got %d raw bytes", sessionCookieEncryptionKeyEnv, len([]byte(key)))
|
||||
}
|
||||
|
||||
func validSessionCookieEncryptionKeyLength(length int) bool {
|
||||
switch length {
|
||||
case 16, 24, 32:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time check that EmbeddedIdPManager implements Manager interface
|
||||
var _ Manager = (*EmbeddedIdPManager)(nil)
|
||||
|
||||
@@ -215,6 +344,7 @@ type EmbeddedIdPManager struct {
|
||||
provider *dex.Provider
|
||||
appMetrics telemetry.AppMetrics
|
||||
config EmbeddedIdPConfig
|
||||
mfaEnabled bool
|
||||
}
|
||||
|
||||
// NewEmbeddedIdPManager creates a new instance of EmbeddedIdPManager from a configuration.
|
||||
@@ -641,6 +771,27 @@ func (m *EmbeddedIdPManager) IsLocalAuthDisabled() bool {
|
||||
return m.config.LocalAuthDisabled
|
||||
}
|
||||
|
||||
// SetMFAEnabled enables or disables TOTP MFA for local users by updating the MFAChain on OAuth2 clients.
|
||||
func (m *EmbeddedIdPManager) SetMFAEnabled(ctx context.Context, enabled bool) error {
|
||||
var mfaChain []string
|
||||
if enabled {
|
||||
mfaChain = []string{"default-totp"}
|
||||
}
|
||||
if err := m.provider.SetClientsMFAChain(ctx, []string{
|
||||
staticClientCLI,
|
||||
staticClientDashboard,
|
||||
}, mfaChain); err != nil {
|
||||
return fmt.Errorf("failed to set MFA enabled=%v: %w", enabled, err)
|
||||
}
|
||||
m.mfaEnabled = enabled
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsMFAEnabled returns whether TOTP MFA is currently enabled for local users.
|
||||
func (m *EmbeddedIdPManager) IsMFAEnabled() bool {
|
||||
return m.mfaEnabled
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -2,6 +2,7 @@ package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -313,6 +314,72 @@ func TestEmbeddedIdPManager_UpdateUserPassword(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPConfig_ToYAMLConfig_SessionCookieEncryptionKey(t *testing.T) {
|
||||
t.Setenv(sessionCookieEncryptionKeyEnv, "")
|
||||
|
||||
rawKey := "0123456789abcdef0123456789abcdef"
|
||||
config := &EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
SessionCookieEncryptionKey: base64.StdEncoding.EncodeToString([]byte(rawKey)),
|
||||
Storage: EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: EmbeddedStorageTypeConfig{
|
||||
File: filepath.Join(t.TempDir(), "dex.db"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
yamlConfig, err := config.ToYAMLConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, yamlConfig.Sessions)
|
||||
assert.Equal(t, rawKey, yamlConfig.Sessions.CookieEncryptionKey)
|
||||
}
|
||||
|
||||
func TestResolveSessionCookieEncryptionKey(t *testing.T) {
|
||||
rawKey := "0123456789abcdef0123456789abcdef"
|
||||
|
||||
t.Run("uses raw configured key", func(t *testing.T) {
|
||||
t.Setenv(sessionCookieEncryptionKeyEnv, "")
|
||||
|
||||
key, err := resolveSessionCookieEncryptionKey(rawKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rawKey, key)
|
||||
})
|
||||
|
||||
t.Run("uses base64 configured key", func(t *testing.T) {
|
||||
t.Setenv(sessionCookieEncryptionKeyEnv, "")
|
||||
|
||||
key, err := resolveSessionCookieEncryptionKey(base64.StdEncoding.EncodeToString([]byte(rawKey)))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rawKey, key)
|
||||
})
|
||||
|
||||
t.Run("falls back to env var", func(t *testing.T) {
|
||||
t.Setenv(sessionCookieEncryptionKeyEnv, rawKey)
|
||||
|
||||
key, err := resolveSessionCookieEncryptionKey("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rawKey, key)
|
||||
})
|
||||
|
||||
t.Run("empty key disables encryption", func(t *testing.T) {
|
||||
t.Setenv(sessionCookieEncryptionKeyEnv, "")
|
||||
|
||||
key, err := resolveSessionCookieEncryptionKey("")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, key)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid key length", func(t *testing.T) {
|
||||
t.Setenv(sessionCookieEncryptionKeyEnv, "")
|
||||
|
||||
_, err := resolveSessionCookieEncryptionKey("32")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), sessionCookieEncryptionKeyEnv)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -236,7 +236,6 @@ func (s *SqlStore) GetPeerJobs(ctx context.Context, accountID, peerID string) ([
|
||||
Where(accountAndPeerIDQueryCondition, accountID, peerID).
|
||||
Order("created_at DESC").
|
||||
Find(&jobs).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to fetch jobs from store: %s", err)
|
||||
return nil, err
|
||||
@@ -463,7 +462,6 @@ func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1514,6 +1512,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
settings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups,
|
||||
settings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,
|
||||
settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled,
|
||||
settings_local_mfa_enabled,
|
||||
-- Embedded ExtraSettings
|
||||
settings_extra_peer_approval_enabled, settings_extra_user_approval_required,
|
||||
settings_extra_integrated_validator, settings_extra_integrated_validator_groups
|
||||
@@ -1535,6 +1534,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
sNetworkRangeV6 sql.NullString
|
||||
sIPv6EnabledGroups sql.NullString
|
||||
sLazyConnectionEnabled sql.NullBool
|
||||
sLocalMFAEnabled sql.NullBool
|
||||
sExtraPeerApprovalEnabled sql.NullBool
|
||||
sExtraUserApprovalRequired sql.NullBool
|
||||
sExtraIntegratedValidator sql.NullString
|
||||
@@ -1557,6 +1557,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,
|
||||
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
|
||||
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
|
||||
&sLocalMFAEnabled,
|
||||
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
|
||||
&sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups,
|
||||
)
|
||||
@@ -1619,6 +1620,9 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
if sLazyConnectionEnabled.Valid {
|
||||
account.Settings.LazyConnectionEnabled = sLazyConnectionEnabled.Bool
|
||||
}
|
||||
if sLocalMFAEnabled.Valid {
|
||||
account.Settings.LocalMfaEnabled = sLocalMFAEnabled.Bool
|
||||
}
|
||||
if sJWTAllowGroups.Valid {
|
||||
_ = json.Unmarshal([]byte(sJWTAllowGroups.String), &account.Settings.JWTAllowGroups)
|
||||
}
|
||||
@@ -3061,7 +3065,6 @@ func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peer
|
||||
GroupID: groupID,
|
||||
PeerID: peerID,
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
return status.Errorf(status.Internal, "error adding peer to group 'All': %v", err)
|
||||
}
|
||||
@@ -3081,7 +3084,6 @@ func (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupI
|
||||
Columns: []clause.Column{{Name: "group_id"}, {Name: "peer_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(peer).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to add peer %s to group %s for account %s: %v", peerID, groupID, accountID, err)
|
||||
return status.Errorf(status.Internal, "failed to add peer to group")
|
||||
@@ -3094,7 +3096,6 @@ func (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupI
|
||||
func (s *SqlStore) RemovePeerFromGroup(ctx context.Context, peerID string, groupID string) error {
|
||||
err := s.db.
|
||||
Delete(&types.GroupPeer{}, "group_id = ? AND peer_id = ?", groupID, peerID).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to remove peer %s from group %s: %v", peerID, groupID, err)
|
||||
return status.Errorf(status.Internal, "failed to remove peer from group")
|
||||
@@ -3107,7 +3108,6 @@ func (s *SqlStore) RemovePeerFromGroup(ctx context.Context, peerID string, group
|
||||
func (s *SqlStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) error {
|
||||
err := s.db.
|
||||
Delete(&types.GroupPeer{}, "peer_id = ?", peerID).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to remove peer %s from all groups: %v", peerID, err)
|
||||
return status.Errorf(status.Internal, "failed to remove peer from all groups")
|
||||
@@ -4964,7 +4964,6 @@ func (s *SqlStore) UpdateService(ctx context.Context, service *rpservice.Service
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to update service to store: %v", err)
|
||||
return status.Errorf(status.Internal, "failed to update service to store")
|
||||
@@ -5620,7 +5619,6 @@ func (s *SqlStore) getClusterUnanimousCapability(ctx context.Context, clusterAdd
|
||||
Where("cluster_address = ? AND status = ? AND last_seen > ?",
|
||||
clusterAddr, "connected", time.Now().Add(-proxyActiveThreshold)).
|
||||
Scan(&result).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("query cluster capability %s for %s: %v", column, clusterAddr, err)
|
||||
return nil
|
||||
@@ -5662,7 +5660,6 @@ func (s *SqlStore) getClusterCapability(ctx context.Context, clusterAddr, column
|
||||
Where("cluster_address = ? AND status = ? AND last_seen > ?",
|
||||
clusterAddr, "connected", time.Now().Add(-proxyActiveThreshold)).
|
||||
Scan(&result).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("query cluster capability %s for %s: %v", column, clusterAddr, err)
|
||||
return nil
|
||||
|
||||
@@ -80,6 +80,10 @@ type Settings struct {
|
||||
// LocalAuthDisabled indicates if local (email/password) authentication is disabled.
|
||||
// This is a runtime-only field, not stored in the database.
|
||||
LocalAuthDisabled bool `gorm:"-"`
|
||||
|
||||
// LocalMfaEnabled indicates if TOTP MFA is enabled for local users.
|
||||
// Only applicable when the embedded IDP is enabled.
|
||||
LocalMfaEnabled bool
|
||||
}
|
||||
|
||||
// Copy copies the Settings struct
|
||||
@@ -108,6 +112,7 @@ func (s *Settings) Copy() *Settings {
|
||||
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
|
||||
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
||||
LocalAuthDisabled: s.LocalAuthDisabled,
|
||||
LocalMfaEnabled: s.LocalMfaEnabled,
|
||||
}
|
||||
if s.Extra != nil {
|
||||
settings.Extra = s.Extra.Copy()
|
||||
|
||||
Reference in New Issue
Block a user