[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
This commit is contained in:
hakansa
2025-07-29 13:03:15 +03:00
committed by GitHub
parent e1c66a8124
commit 8c8473aed3
9 changed files with 117 additions and 32 deletions

View File

@ -72,6 +72,7 @@ var (
anonymizeFlag bool anonymizeFlag bool
dnsRouteInterval time.Duration dnsRouteInterval time.Duration
lazyConnEnabled bool lazyConnEnabled bool
profilesDisabled bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "netbird", Use: "netbird",

View File

@ -42,6 +42,7 @@ func init() {
} }
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd) 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") rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
serviceEnvDesc := `Sets extra environment variables for the service. ` + serviceEnvDesc := `Sets extra environment variables for the service. ` +

View File

@ -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 { if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err) log.Fatalf("failed to start daemon: %v", err)
} }

View File

@ -134,7 +134,7 @@ func startClientDaemon(
s := grpc.NewServer() s := grpc.NewServer()
server := client.New(ctx, server := client.New(ctx,
"") "", false)
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -44,6 +44,7 @@ const (
defaultRetryMultiplier = 1.7 defaultRetryMultiplier = 1.7
errRestoreResidualState = "failed to restore residual state: %v" errRestoreResidualState = "failed to restore residual state: %v"
errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled"
) )
// Server for service control. // Server for service control.
@ -69,6 +70,7 @@ type Server struct {
isSessionActive atomic.Bool isSessionActive atomic.Bool
profileManager profilemanager.ServiceManager profileManager profilemanager.ServiceManager
profilesDisabled bool
} }
type oauthAuthFlow struct { type oauthAuthFlow struct {
@ -79,13 +81,14 @@ type oauthAuthFlow struct {
} }
// New server instance constructor. // New server instance constructor.
func New(ctx context.Context, logFile string) *Server { func New(ctx context.Context, logFile string, profilesDisabled bool) *Server {
return &Server{ return &Server{
rootCtx: ctx, rootCtx: ctx,
logFile: logFile, logFile: logFile,
persistNetworkMap: true, persistNetworkMap: true,
statusRecorder: peer.NewRecorder(""), statusRecorder: peer.NewRecorder(""),
profileManager: profilemanager.ServiceManager{}, profileManager: profilemanager.ServiceManager{},
profilesDisabled: profilesDisabled,
} }
} }
@ -320,6 +323,10 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.checkProfilesDisabled() {
return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled)
}
profState := profilemanager.ActiveProfileState{ profState := profilemanager.ActiveProfileState{
Name: msg.ProfileName, Name: msg.ProfileName,
Username: msg.Username, Username: msg.Username,
@ -737,6 +744,11 @@ func (s *Server) switchProfileIfNeeded(profileName string, userName *string, act
} }
if profileName != activeProf.Name || username != activeProf.Username { 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) log.Infof("switching to profile %s for user %s", profileName, username)
if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{ if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: profileName, Name: profileName,
@ -1069,6 +1081,10 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) (
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.checkProfilesDisabled() {
return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled)
}
if msg.ProfileName == "" || msg.Username == "" { if msg.ProfileName == "" || msg.Username == "" {
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided") 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() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.checkProfilesDisabled() {
return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled)
}
if msg.ProfileName == "" { if msg.ProfileName == "" {
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") 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, Username: activeProfile.Username,
}, nil }, 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
}

View File

@ -94,7 +94,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
t.Fatalf("failed to set active profile state: %v", err) t.Fatalf("failed to set active profile state: %v", err)
} }
s := New(ctx, "debug") s := New(ctx, "debug", false)
s.config = config s.config = config
@ -151,7 +151,7 @@ func TestServer_Up(t *testing.T) {
t.Fatalf("failed to set active profile state: %v", err) t.Fatalf("failed to set active profile state: %v", err)
} }
s := New(ctx, "console") s := New(ctx, "console", false)
err = s.Start() err = s.Start()
require.NoError(t, err) require.NoError(t, err)
@ -227,7 +227,7 @@ func TestServer_SubcribeEvents(t *testing.T) {
t.Fatalf("failed to set active profile state: %v", err) t.Fatalf("failed to set active profile state: %v", err)
} }
s := New(ctx, "console") s := New(ctx, "console", false)
err = s.Start() err = s.Start()
require.NoError(t, err) require.NoError(t, err)

View File

@ -802,7 +802,21 @@ func (s *serviceClient) onTrayReady() {
profileMenuItem := systray.AddMenuItem("", "") profileMenuItem := systray.AddMenuItem("", "")
emailMenuItem := 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() systray.AddSeparator()
s.mUp = systray.AddMenuItem("Connect", "Connect") s.mUp = systray.AddMenuItem("Connect", "Connect")

View File

@ -86,35 +86,60 @@ func (h *eventHandler) handleDisconnectClick() {
func (h *eventHandler) handleAllowSSHClick() { func (h *eventHandler) handleAllowSSHClick() {
h.toggleCheckbox(h.client.mAllowSSH) 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() { func (h *eventHandler) handleAutoConnectClick() {
h.toggleCheckbox(h.client.mAutoConnect) 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() { func (h *eventHandler) handleRosenpassClick() {
h.toggleCheckbox(h.client.mEnableRosenpass) 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() { func (h *eventHandler) handleLazyConnectionClick() {
h.toggleCheckbox(h.client.mLazyConnEnabled) 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() { func (h *eventHandler) handleBlockInboundClick() {
h.toggleCheckbox(h.client.mBlockInbound) 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() { func (h *eventHandler) handleNotificationsClick() {
h.toggleCheckbox(h.client.mNotifications) 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.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
} }
h.updateConfigWithErr()
} }
func (h *eventHandler) handleAdvancedSettingsClick() { 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 { 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) { func (h *eventHandler) runSelfCommand(ctx context.Context, command, arg string) {

View File

@ -334,7 +334,7 @@ type profileMenu struct {
mu sync.Mutex mu sync.Mutex
ctx context.Context ctx context.Context
profileManager *profilemanager.ProfileManager profileManager *profilemanager.ProfileManager
eventHandler eventHandler eventHandler *eventHandler
profileMenuItem *systray.MenuItem profileMenuItem *systray.MenuItem
emailMenuItem *systray.MenuItem emailMenuItem *systray.MenuItem
profileSubItems []*subItem profileSubItems []*subItem
@ -344,24 +344,34 @@ type profileMenu struct {
upClickCallback func() error upClickCallback func() error
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error) getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
loadSettingsCallback func() 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, func newProfileMenu(args newProfileMenuArgs) *profileMenu {
downClickCallback, upClickCallback func() error,
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error),
loadSettingsCallback func()) *profileMenu {
p := profileMenu{ p := profileMenu{
ctx: ctx, ctx: args.ctx,
profileManager: profileManager, profileManager: args.profileManager,
eventHandler: eventHandler, eventHandler: args.eventHandler,
profileMenuItem: profileMenuItem, profileMenuItem: args.profileMenuItem,
emailMenuItem: emailMenuItem, emailMenuItem: args.emailMenuItem,
downClickCallback: downClickCallback, downClickCallback: args.downClickCallback,
upClickCallback: upClickCallback, upClickCallback: args.upClickCallback,
getSrvClientCallback: getSrvClientCallback, getSrvClientCallback: args.getSrvClientCallback,
loadSettingsCallback: loadSettingsCallback, loadSettingsCallback: args.loadSettingsCallback,
app: args.app,
} }
p.emailMenuItem.Disable() p.emailMenuItem.Disable()
@ -479,6 +489,8 @@ func (p *profileMenu) refresh() {
}) })
if err != nil { if err != nil {
log.Errorf("failed to switch profile: %v", err) log.Errorf("failed to switch profile: %v", err)
// show notification dialog
p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile"))
return return
} }