From dcd7cd3c6af4c6dfae2a6d491e37b5cc60f54482 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Thu, 20 Feb 2025 15:55:42 -0800 Subject: [PATCH] client/systray: show message on localapi permission error When LocalAPI returns an AccessDeniedError, display a message in the menu and hide or disable most other menu items. This currently includes a placeholder KB link which I'll update if we end up using something different. I debated whether to change the app icon to indicate an error, but opted not to since there is actually nothing wrong with the client itself and Tailscale will continue to function normally. It's just that the systray app itself is in a read-only state. Updates #1708 Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris --- client/systray/systray.go | 64 ++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/client/systray/systray.go b/client/systray/systray.go index ac64b9958..b5bde551c 100644 --- a/client/systray/systray.go +++ b/client/systray/systray.go @@ -72,6 +72,11 @@ type Menu struct { curProfile ipn.LoginProfile allProfiles []ipn.LoginProfile + // readonly is whether the systray app is running in read-only mode. + // This is set if LocalAPI returns a permission error, + // typically because the user needs to run `tailscale set --operator=$USER`. + readonly bool + bgCtx context.Context // ctx for background tasks not involving menu item clicks bgCancel context.CancelFunc @@ -153,6 +158,8 @@ func (menu *Menu) updateState() { defer menu.mu.Unlock() menu.init() + menu.readonly = false + var err error menu.status, err = menu.lc.Status(menu.bgCtx) if err != nil { @@ -160,6 +167,9 @@ func (menu *Menu) updateState() { } menu.curProfile, menu.allProfiles, err = menu.lc.ProfileStatus(menu.bgCtx) if err != nil { + if local.IsAccessDeniedError(err) { + menu.readonly = true + } log.Print(err) } } @@ -182,6 +192,15 @@ func (menu *Menu) rebuild() { systray.ResetMenu() + if menu.readonly { + const readonlyMsg = "No permission to manage Tailscale.\nSee tailscale.com/s/cli-operator" + m := systray.AddMenuItem(readonlyMsg, "") + onClick(ctx, m, func(_ context.Context) { + webbrowser.Open("https://tailscale.com/s/cli-operator") + }) + systray.AddSeparator() + } + menu.connect = systray.AddMenuItem("Connect", "") menu.disconnect = systray.AddMenuItem("Disconnect", "") menu.disconnect.Hide() @@ -222,28 +241,35 @@ func (menu *Menu) rebuild() { setAppIcon(disconnected) } + if menu.readonly { + menu.connect.Disable() + menu.disconnect.Disable() + } + account := "Account" if pt := profileTitle(menu.curProfile); pt != "" { account = pt } - accounts := systray.AddMenuItem(account, "") - setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL) - time.Sleep(newMenuDelay) - for _, profile := range menu.allProfiles { - title := profileTitle(profile) - var item *systray.MenuItem - if profile.ID == menu.curProfile.ID { - item = accounts.AddSubMenuItemCheckbox(title, "", true) - } else { - item = accounts.AddSubMenuItem(title, "") - } - setRemoteIcon(item, profile.UserProfile.ProfilePicURL) - onClick(ctx, item, func(ctx context.Context) { - select { - case <-ctx.Done(): - case menu.accountsCh <- profile.ID: + if !menu.readonly { + accounts := systray.AddMenuItem(account, "") + setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL) + time.Sleep(newMenuDelay) + for _, profile := range menu.allProfiles { + title := profileTitle(profile) + var item *systray.MenuItem + if profile.ID == menu.curProfile.ID { + item = accounts.AddSubMenuItemCheckbox(title, "", true) + } else { + item = accounts.AddSubMenuItem(title, "") } - }) + setRemoteIcon(item, profile.UserProfile.ProfilePicURL) + onClick(ctx, item, func(ctx context.Context) { + select { + case <-ctx.Done(): + case menu.accountsCh <- profile.ID: + } + }) + } } if menu.status != nil && menu.status.Self != nil && len(menu.status.Self.TailscaleIPs) > 0 { @@ -255,7 +281,9 @@ func (menu *Menu) rebuild() { } systray.AddSeparator() - menu.rebuildExitNodeMenu(ctx) + if !menu.readonly { + menu.rebuildExitNodeMenu(ctx) + } if menu.status != nil { menu.more = systray.AddMenuItem("More settings", "")