From 8c8473aed38631c63ddafd85f39c5fb99c42cb4f Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:03:15 +0300 Subject: [PATCH] [client] Add support for disabling profiles feature via command line flag (#4235) * Add support for disabling profiles feature via command line flag * Add profiles disabling flag to service command * Refactor profile menu initialization and enhance error notifications in event handlers --- client/cmd/root.go | 1 + client/cmd/service.go | 1 + client/cmd/service_controller.go | 2 +- client/cmd/testutil_test.go | 2 +- client/server/server.go | 34 ++++++++++++++++++++++-- client/server/server_test.go | 6 ++--- client/ui/client_ui.go | 16 +++++++++++- client/ui/event_handler.go | 45 +++++++++++++++++++++++++------- client/ui/profile.go | 42 ++++++++++++++++++----------- 9 files changed, 117 insertions(+), 32 deletions(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 8e8ee3280..e3ce79964 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -72,6 +72,7 @@ var ( anonymizeFlag bool dnsRouteInterval time.Duration lazyConnEnabled bool + profilesDisabled bool rootCmd = &cobra.Command{ Use: "netbird", diff --git a/client/cmd/service.go b/client/cmd/service.go index 178f4bf0e..d8745f1c4 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -42,6 +42,7 @@ func init() { } serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd) + serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile.") rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") serviceEnvDesc := `Sets extra environment variables for the service. ` + diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index cbffff797..6dc6bca9b 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles)) + serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), profilesDisabled) if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index cf94754c1..5dbc8cd7f 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -134,7 +134,7 @@ func startClientDaemon( s := grpc.NewServer() server := client.New(ctx, - "") + "", false) if err := server.Start(); err != nil { t.Fatal(err) } diff --git a/client/server/server.go b/client/server/server.go index f3414888d..80cd6078f 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -44,6 +44,7 @@ const ( defaultRetryMultiplier = 1.7 errRestoreResidualState = "failed to restore residual state: %v" + errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled" ) // Server for service control. @@ -68,7 +69,8 @@ type Server struct { persistNetworkMap bool isSessionActive atomic.Bool - profileManager profilemanager.ServiceManager + profileManager profilemanager.ServiceManager + profilesDisabled bool } type oauthAuthFlow struct { @@ -79,13 +81,14 @@ type oauthAuthFlow struct { } // New server instance constructor. -func New(ctx context.Context, logFile string) *Server { +func New(ctx context.Context, logFile string, profilesDisabled bool) *Server { return &Server{ rootCtx: ctx, logFile: logFile, persistNetworkMap: true, statusRecorder: peer.NewRecorder(""), profileManager: profilemanager.ServiceManager{}, + profilesDisabled: profilesDisabled, } } @@ -320,6 +323,10 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques s.mutex.Lock() defer s.mutex.Unlock() + if s.checkProfilesDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + profState := profilemanager.ActiveProfileState{ Name: msg.ProfileName, Username: msg.Username, @@ -737,6 +744,11 @@ func (s *Server) switchProfileIfNeeded(profileName string, userName *string, act } if profileName != activeProf.Name || username != activeProf.Username { + if s.checkProfilesDisabled() { + log.Errorf("profiles are disabled, you cannot use this feature without profiles enabled") + return gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + log.Infof("switching to profile %s for user %s", profileName, username) if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ Name: profileName, @@ -1069,6 +1081,10 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) ( s.mutex.Lock() defer s.mutex.Unlock() + if s.checkProfilesDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + if msg.ProfileName == "" || msg.Username == "" { return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided") } @@ -1086,6 +1102,10 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ s.mutex.Lock() defer s.mutex.Unlock() + if s.checkProfilesDisabled() { + return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled) + } + if msg.ProfileName == "" { return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") } @@ -1142,3 +1162,13 @@ func (s *Server) GetActiveProfile(ctx context.Context, msg *proto.GetActiveProfi Username: activeProfile.Username, }, nil } + +func (s *Server) checkProfilesDisabled() bool { + // Check if the environment variable is set to disable profiles + if s.profilesDisabled { + log.Warn("Profiles are disabled via NB_DISABLE_PROFILES environment variable") + return true + } + + return false +} diff --git a/client/server/server_test.go b/client/server/server_test.go index dda610076..afd38b4a4 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -94,7 +94,7 @@ func TestConnectWithRetryRuns(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "debug") + s := New(ctx, "debug", false) s.config = config @@ -151,7 +151,7 @@ func TestServer_Up(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console") + s := New(ctx, "console", false) err = s.Start() require.NoError(t, err) @@ -227,7 +227,7 @@ func TestServer_SubcribeEvents(t *testing.T) { t.Fatalf("failed to set active profile state: %v", err) } - s := New(ctx, "console") + s := New(ctx, "console", false) err = s.Start() require.NoError(t, err) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 781ec56f8..c74412c8b 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -802,7 +802,21 @@ func (s *serviceClient) onTrayReady() { profileMenuItem := systray.AddMenuItem("", "") emailMenuItem := systray.AddMenuItem("", "") - s.mProfile = newProfileMenu(s.ctx, s.profileManager, *s.eventHandler, profileMenuItem, emailMenuItem, s.menuDownClick, s.menuUpClick, s.getSrvClient, s.loadSettings) + + newProfileMenuArgs := &newProfileMenuArgs{ + ctx: s.ctx, + profileManager: s.profileManager, + eventHandler: s.eventHandler, + profileMenuItem: profileMenuItem, + emailMenuItem: emailMenuItem, + downClickCallback: s.menuDownClick, + upClickCallback: s.menuUpClick, + getSrvClientCallback: s.getSrvClient, + loadSettingsCallback: s.loadSettings, + app: s.app, + } + + s.mProfile = newProfileMenu(*newProfileMenuArgs) systray.AddSeparator() s.mUp = systray.AddMenuItem("Connect", "Connect") diff --git a/client/ui/event_handler.go b/client/ui/event_handler.go index 39ea3867c..c0bc74a2c 100644 --- a/client/ui/event_handler.go +++ b/client/ui/event_handler.go @@ -86,35 +86,60 @@ func (h *eventHandler) handleDisconnectClick() { func (h *eventHandler) handleAllowSSHClick() { h.toggleCheckbox(h.client.mAllowSSH) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update SSH settings")) + } + } func (h *eventHandler) handleAutoConnectClick() { h.toggleCheckbox(h.client.mAutoConnect) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update auto-connect settings")) + } } func (h *eventHandler) handleRosenpassClick() { h.toggleCheckbox(h.client.mEnableRosenpass) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update Rosenpass settings")) + } } func (h *eventHandler) handleLazyConnectionClick() { h.toggleCheckbox(h.client.mLazyConnEnabled) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update lazy connection settings")) + } } func (h *eventHandler) handleBlockInboundClick() { h.toggleCheckbox(h.client.mBlockInbound) - h.updateConfigWithErr() + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update block inbound settings")) + } } func (h *eventHandler) handleNotificationsClick() { h.toggleCheckbox(h.client.mNotifications) - if h.client.eventManager != nil { + if err := h.updateConfigWithErr(); err != nil { + h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error + log.Errorf("failed to update config: %v", err) + h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update notifications settings")) + } else if h.client.eventManager != nil { h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked()) } - h.updateConfigWithErr() + } func (h *eventHandler) handleAdvancedSettingsClick() { @@ -166,10 +191,12 @@ func (h *eventHandler) toggleCheckbox(item *systray.MenuItem) { } } -func (h *eventHandler) updateConfigWithErr() { +func (h *eventHandler) updateConfigWithErr() error { if err := h.client.updateConfig(); err != nil { - log.Errorf("failed to update config: %v", err) + return err } + + return nil } func (h *eventHandler) runSelfCommand(ctx context.Context, command, arg string) { diff --git a/client/ui/profile.go b/client/ui/profile.go index 142582c25..779f60aa4 100644 --- a/client/ui/profile.go +++ b/client/ui/profile.go @@ -334,7 +334,7 @@ type profileMenu struct { mu sync.Mutex ctx context.Context profileManager *profilemanager.ProfileManager - eventHandler eventHandler + eventHandler *eventHandler profileMenuItem *systray.MenuItem emailMenuItem *systray.MenuItem profileSubItems []*subItem @@ -344,24 +344,34 @@ type profileMenu struct { upClickCallback func() error getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) loadSettingsCallback func() + app fyne.App } -func newProfileMenu(ctx context.Context, profileManager *profilemanager.ProfileManager, +type newProfileMenuArgs struct { + ctx context.Context + profileManager *profilemanager.ProfileManager + eventHandler *eventHandler + profileMenuItem *systray.MenuItem + emailMenuItem *systray.MenuItem + downClickCallback func() error + upClickCallback func() error + getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) + loadSettingsCallback func() + app fyne.App +} - eventHandler eventHandler, profileMenuItem, emailMenuItem *systray.MenuItem, - downClickCallback, upClickCallback func() error, - getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error), - loadSettingsCallback func()) *profileMenu { +func newProfileMenu(args newProfileMenuArgs) *profileMenu { p := profileMenu{ - ctx: ctx, - profileManager: profileManager, - eventHandler: eventHandler, - profileMenuItem: profileMenuItem, - emailMenuItem: emailMenuItem, - downClickCallback: downClickCallback, - upClickCallback: upClickCallback, - getSrvClientCallback: getSrvClientCallback, - loadSettingsCallback: loadSettingsCallback, + ctx: args.ctx, + profileManager: args.profileManager, + eventHandler: args.eventHandler, + profileMenuItem: args.profileMenuItem, + emailMenuItem: args.emailMenuItem, + downClickCallback: args.downClickCallback, + upClickCallback: args.upClickCallback, + getSrvClientCallback: args.getSrvClientCallback, + loadSettingsCallback: args.loadSettingsCallback, + app: args.app, } p.emailMenuItem.Disable() @@ -479,6 +489,8 @@ func (p *profileMenu) refresh() { }) if err != nil { log.Errorf("failed to switch profile: %v", err) + // show notification dialog + p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile")) return }