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 <will@tailscale.com>
This commit is contained in:
Will Norris 2025-02-20 15:55:42 -08:00 committed by Will Norris
parent 074372d6c5
commit dcd7cd3c6a

View File

@ -72,6 +72,11 @@ type Menu struct {
curProfile ipn.LoginProfile curProfile ipn.LoginProfile
allProfiles []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 bgCtx context.Context // ctx for background tasks not involving menu item clicks
bgCancel context.CancelFunc bgCancel context.CancelFunc
@ -153,6 +158,8 @@ func (menu *Menu) updateState() {
defer menu.mu.Unlock() defer menu.mu.Unlock()
menu.init() menu.init()
menu.readonly = false
var err error var err error
menu.status, err = menu.lc.Status(menu.bgCtx) menu.status, err = menu.lc.Status(menu.bgCtx)
if err != nil { if err != nil {
@ -160,6 +167,9 @@ func (menu *Menu) updateState() {
} }
menu.curProfile, menu.allProfiles, err = menu.lc.ProfileStatus(menu.bgCtx) menu.curProfile, menu.allProfiles, err = menu.lc.ProfileStatus(menu.bgCtx)
if err != nil { if err != nil {
if local.IsAccessDeniedError(err) {
menu.readonly = true
}
log.Print(err) log.Print(err)
} }
} }
@ -182,6 +192,15 @@ func (menu *Menu) rebuild() {
systray.ResetMenu() 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.connect = systray.AddMenuItem("Connect", "")
menu.disconnect = systray.AddMenuItem("Disconnect", "") menu.disconnect = systray.AddMenuItem("Disconnect", "")
menu.disconnect.Hide() menu.disconnect.Hide()
@ -222,10 +241,16 @@ func (menu *Menu) rebuild() {
setAppIcon(disconnected) setAppIcon(disconnected)
} }
if menu.readonly {
menu.connect.Disable()
menu.disconnect.Disable()
}
account := "Account" account := "Account"
if pt := profileTitle(menu.curProfile); pt != "" { if pt := profileTitle(menu.curProfile); pt != "" {
account = pt account = pt
} }
if !menu.readonly {
accounts := systray.AddMenuItem(account, "") accounts := systray.AddMenuItem(account, "")
setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL) setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL)
time.Sleep(newMenuDelay) time.Sleep(newMenuDelay)
@ -245,6 +270,7 @@ func (menu *Menu) rebuild() {
} }
}) })
} }
}
if menu.status != nil && menu.status.Self != nil && len(menu.status.Self.TailscaleIPs) > 0 { if menu.status != nil && menu.status.Self != nil && len(menu.status.Self.TailscaleIPs) > 0 {
title := fmt.Sprintf("This Device: %s (%s)", menu.status.Self.HostName, menu.status.Self.TailscaleIPs[0]) title := fmt.Sprintf("This Device: %s (%s)", menu.status.Self.HostName, menu.status.Self.TailscaleIPs[0])
@ -255,7 +281,9 @@ func (menu *Menu) rebuild() {
} }
systray.AddSeparator() systray.AddSeparator()
if !menu.readonly {
menu.rebuildExitNodeMenu(ctx) menu.rebuildExitNodeMenu(ctx)
}
if menu.status != nil { if menu.status != nil {
menu.more = systray.AddMenuItem("More settings", "") menu.more = systray.AddMenuItem("More settings", "")