mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-15 07:20:36 -04:00
ipn/ipnlocal: keep operator user when switching to a new profile
"tailscale login" first switches to a new empty profile before starting the login flow. Switching reset the prefs to defaults, clearing OperatorUser, so a non-root operator lost LocalAPI write access partway through the command: the profile switch itself succeeded, then the following check-prefs request failed with "checkprefs access denied". Worse, the operator setting was lost entirely, so subsequent attempts failed earlier with "profiles access denied". This made the long-suggested advice of running "sudo tailscale set --operator=$USER" not work for logging in. Carry OperatorUser over to the new profile's prefs. It is a machine-local administrative setting rather than an account setting, and only actors that already have write access can initiate a profile switch, so this grants no new access. Fixes #18294 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: Ia4c4336d211e489dd268b428aad3a0bf55e70ad8
This commit is contained in:
1 parent
7dec0c7d7a
commit
c14f706404
2 files changed
+65
No files matched your search
@@ -139,6 +139,18 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
|
||||
// and must check whether pm.currentProfile is Valid before using it.
|
||||
func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.LoginProfileView, changed bool, err error) {
|
||||
prefs := defaultPrefs
|
||||
if pm.prefs.Valid() && pm.prefs.OperatorUser() != "" {
|
||||
// Carry the operator over to a new profile. The operator is a
|
||||
// machine-local administrative setting rather than an account
|
||||
// setting, and any actor able to initiate a profile switch
|
||||
// already has write access. Without this, an operator running
|
||||
// "tailscale login" would lose access partway through the
|
||||
// login flow when it switches to a new empty profile.
|
||||
// See tailscale/tailscale#18294.
|
||||
p := prefs.AsStruct()
|
||||
p.OperatorUser = pm.prefs.OperatorUser()
|
||||
prefs = p.View()
|
||||
}
|
||||
switch {
|
||||
case !profile.Valid():
|
||||
// Create a new profile that is not associated with any user.
|
||||
|
||||
@@ -492,6 +492,59 @@ func TestProfileManagement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSwitchToNewProfileKeepsOperator tests that the operator user carries
|
||||
// over when switching to a new, empty profile, as during "tailscale login".
|
||||
// See tailscale/tailscale#18294: switching to a new profile used to reset
|
||||
// the prefs to defaults, locking the operator out partway through the login
|
||||
// flow and losing the operator setting entirely.
|
||||
func TestSwitchToNewProfileKeepsOperator(t *testing.T) {
|
||||
store := new(mem.Store)
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, health.NewTracker(eventbustest.NewBus(t)), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set the operator on the initial empty profile,
|
||||
// as "sudo tailscale set --operator=$USER" would.
|
||||
prefs := pm.CurrentPrefs().AsStruct()
|
||||
prefs.OperatorUser = "operator"
|
||||
if err := pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pm.SwitchToNewProfile()
|
||||
if got := pm.CurrentPrefs().OperatorUser(); got != "operator" {
|
||||
t.Errorf("OperatorUser after switch from empty profile = %q; want %q", got, "operator")
|
||||
}
|
||||
|
||||
// Log in and change another pref, then switch to a new profile again.
|
||||
// The operator should carry over, but other prefs should reset.
|
||||
prefs = pm.CurrentPrefs().AsStruct()
|
||||
prefs.Persist = &persist.Persist{
|
||||
PrivateNodeKey: key.NewNode(),
|
||||
UserProfile: tailcfg.UserProfile{
|
||||
ID: 1,
|
||||
LoginName: "user@example.com",
|
||||
},
|
||||
NodeID: "node1",
|
||||
}
|
||||
prefs.Hostname = "buffalo"
|
||||
if err := pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pm.SwitchToNewProfile()
|
||||
if id := pm.CurrentProfile().ID(); id != "" {
|
||||
t.Errorf("CurrentProfile().ID() after switch = %q; want empty", id)
|
||||
}
|
||||
if got := pm.CurrentPrefs().OperatorUser(); got != "operator" {
|
||||
t.Errorf("OperatorUser after switch from logged-in profile = %q; want %q", got, "operator")
|
||||
}
|
||||
if got := pm.CurrentPrefs().Hostname(); got != "" {
|
||||
t.Errorf("Hostname after switch = %q; want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileManagementWindows tests going into and out of Unattended mode on
|
||||
// Windows.
|
||||
func TestProfileManagementWindows(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user