[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
dnsRouteInterval time.Duration
lazyConnEnabled bool
profilesDisabled bool
rootCmd = &cobra.Command{
Use: "netbird",

View File

@ -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. ` +

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 {
log.Fatalf("failed to start daemon: %v", err)
}

View File

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

View File

@ -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.
@ -69,6 +70,7 @@ type Server struct {
isSessionActive atomic.Bool
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
}

View File

@ -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)

View File

@ -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")

View File

@ -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) {

View File

@ -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
}