diff --git a/ipn/ipnlocal/desktop_sessions.go b/ipn/ipnlocal/desktop_sessions.go index 4e9eebf34..29cb196c7 100644 --- a/ipn/ipnlocal/desktop_sessions.go +++ b/ipn/ipnlocal/desktop_sessions.go @@ -109,37 +109,39 @@ func (e *desktopSessionsExt) updateDesktopSessionState(session *desktop.Session) // getBackgroundProfile is a [profileResolver] that works as follows: // -// If Always-On mode is disabled, it returns no profile ("","",false). +// If Always-On mode is disabled, it returns no profile. // // If AlwaysOn mode is enabled, it returns the current profile unless: -// - The current user has signed out. +// - The current profile's owner has signed out. // - Another user has a foreground (i.e. active/unlocked) session. // -// If the current user's session runs in the background and no other user +// If the current profile owner's session runs in the background and no other user // has a foreground session, it returns the current profile. This applies // when a locally signed-in user locks their screen or when a remote user // disconnects without signing out. // -// In all other cases, it returns no profile ("","",false). +// In all other cases, it returns no profile. // // It is called with [LocalBackend.mu] locked. -func (e *desktopSessionsExt) getBackgroundProfile() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool) { +func (e *desktopSessionsExt) getBackgroundProfile() ipn.LoginProfileView { e.mu.Lock() defer e.mu.Unlock() if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn { - return "", "", false + // If the Always-On mode is disabled, there's no background profile + // as far as the desktop session extension is concerned. + return ipn.LoginProfileView{} } - isCurrentUserSingedIn := false + isCurrentProfileOwnerSignedIn := false var foregroundUIDs []ipn.WindowsUserID for _, s := range e.id2sess { switch uid := s.User.UserID(); uid { - case e.pm.CurrentUserID(): - isCurrentUserSingedIn = true + case e.pm.CurrentProfile().LocalUserID(): + isCurrentProfileOwnerSignedIn = true if s.Status == desktop.ForegroundSession { // Keep the current profile if the user has a foreground session. - return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true + return e.pm.CurrentProfile() } default: if s.Status == desktop.ForegroundSession { @@ -148,23 +150,24 @@ func (e *desktopSessionsExt) getBackgroundProfile() (_ ipn.WindowsUserID, _ ipn. } } - // If there's no current user (e.g., tailscaled just started), or if the current - // user has no foreground session, switch to the default profile of the first user - // with a foreground session, if any. + // If the current profile is empty and not owned by anyone (e.g., tailscaled just started), + // or if the current profile's owner has no foreground session, switch to the default profile + // of the first user with a foreground session, if any. for _, uid := range foregroundUIDs { - if profileID := e.pm.DefaultUserProfileID(uid); profileID != "" { - return uid, profileID, true + if profile := e.pm.DefaultUserProfile(uid); profile.ID() != "" { + return profile } } - // If no user has a foreground session but the current user is still signed in, + // If no user has a foreground session but the current profile's owner is still signed in, // keep the current profile even if the session is not in the foreground, // such as when the screen is locked or a remote session is disconnected. - if len(foregroundUIDs) == 0 && isCurrentUserSingedIn { - return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true + if len(foregroundUIDs) == 0 && isCurrentProfileOwnerSignedIn { + return e.pm.CurrentProfile() } - return "", "", false + // Otherwise, there's no background profile. + return ipn.LoginProfileView{} } // Shutdown implements [localBackendExtension]. diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index cf71b80fa..a99d67cda 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -204,13 +204,12 @@ func RegisterExtension(name string, newExt NewExtensionFn) { mak.Set(®isteredExtensions, name, newExt) } -// profileResolver is any function that returns user and profile IDs -// along with a flag indicating whether it succeeded. Since an empty -// profile ID ("") represents an empty profile, the ok return parameter -// distinguishes between an empty profile and no profile. +// profileResolver is any function that returns a read-only view of a login profile. +// An invalid view indicates no profile. A valid profile view with an empty [ipn.ProfileID] +// indicates that the profile is new and has not been persisted yet. // // It is called with [LocalBackend.mu] held. -type profileResolver func() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool) +type profileResolver func() ipn.LoginProfileView // NewControlClientCallback is a function to be called when a new [controlclient.Client] // is created and before it is first used. The login profile and prefs represent @@ -4006,13 +4005,21 @@ func (b *LocalBackend) SwitchToBestProfile(reason string) { func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock unlockOnce) { defer unlock() oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault() - uid, profileID, background := b.resolveBestProfileLocked() - cp, switched := b.pm.SetCurrentUserAndProfile(uid, profileID) + profile, background := b.resolveBestProfileLocked() + cp, switched, err := b.pm.SwitchToProfile(profile) switch { case !switched && cp.ID() == "": - b.logf("%s: staying on empty profile", reason) + if err != nil { + b.logf("%s: an error occurred; staying on empty profile: %v", reason, err) + } else { + b.logf("%s: staying on empty profile", reason) + } case !switched: - b.logf("%s: staying on profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID()) + if err != nil { + b.logf("%s: an error occurred; staying on profile %q (%s): %v", reason, cp.UserProfile().LoginName, cp.ID(), err) + } else { + b.logf("%s: staying on profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID()) + } case cp.ID() == "": b.logf("%s: disconnecting Tailscale", reason) case background: @@ -4032,7 +4039,7 @@ func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock un // the TKA initialization or [LocalBackend.Start] can fail. // These errors are not critical as far as we're concerned. // But maybe we should post a notification to the API watchers? - b.logf("failed switching profile to %q: %v", profileID, err) + b.logf("failed switching profile to %q: %v", profile.ID(), err) } } @@ -4041,30 +4048,29 @@ func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock un // the unattended mode is enabled, the current state of the desktop sessions, // and other factors. // -// It returns the user ID, profile ID, and whether the returned profile is -// considered a background profile. A background profile is used when no OS user -// is actively using Tailscale, such as when no GUI/CLI client is connected -// and Unattended Mode is enabled (see also [LocalBackend.getBackgroundProfileLocked]). -// An empty profile ID indicates that Tailscale should switch to an empty profile. +// It returns a read-only view of the profile and whether it is considered +// a background profile. A background profile is used when no OS user is actively +// using Tailscale, such as when no GUI/CLI client is connected and Unattended Mode +// is enabled (see also [LocalBackend.getBackgroundProfileLocked]). +// +// An invalid view indicates no profile, meaning Tailscale should disconnect +// and remain idle until a GUI or CLI client connects. +// A valid profile view with an empty [ipn.ProfileID] indicates a new profile that +// has not been persisted yet. // // b.mu must be held. -func (b *LocalBackend) resolveBestProfileLocked() (userID ipn.WindowsUserID, profileID ipn.ProfileID, isBackground bool) { +func (b *LocalBackend) resolveBestProfileLocked() (_ ipn.LoginProfileView, isBackground bool) { // If a GUI/CLI client is connected, use the connected user's profile, which means // either the current profile if owned by the user, or their default profile. if b.currentUser != nil { - cp := b.pm.CurrentProfile() - uid := b.currentUser.UserID() - - var profileID ipn.ProfileID + profile := b.pm.CurrentProfile() // TODO(nickkhyl): check if the current profile is allowed on the device, // such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet. // See tailscale/corp#26249. - if cp.LocalUserID() == uid { - profileID = cp.ID() - } else { - profileID = b.pm.DefaultUserProfileID(uid) + if uid := b.currentUser.UserID(); profile.LocalUserID() != uid { + profile = b.pm.DefaultUserProfile(uid) } - return uid, profileID, false + return profile, false } // Otherwise, if on Windows, use the background profile if one is set. @@ -4073,8 +4079,8 @@ func (b *LocalBackend) resolveBestProfileLocked() (userID ipn.WindowsUserID, pro // If the returned background profileID is "", Tailscale will disconnect // and remain idle until a GUI or CLI client connects. if goos := envknob.GOOS(); goos == "windows" { - uid, profileID := b.getBackgroundProfileLocked() - return uid, profileID, true + profile := b.getBackgroundProfileLocked() + return profile, true } // On other platforms, however, Tailscale continues to run in the background @@ -4083,7 +4089,7 @@ func (b *LocalBackend) resolveBestProfileLocked() (userID ipn.WindowsUserID, pro // TODO(nickkhyl): check if the current profile is allowed on the device, // such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet. // See tailscale/corp#26249. - return b.pm.CurrentUserID(), b.pm.CurrentProfile().ID(), false + return b.pm.CurrentProfile(), false } // RegisterBackgroundProfileResolver registers a function to be used when @@ -4100,30 +4106,31 @@ func (b *LocalBackend) RegisterBackgroundProfileResolver(resolver profileResolve } } -// getBackgroundProfileLocked returns the user and profile ID to use when no GUI/CLI -// client is connected, or "","" if Tailscale should not run in the background. +// getBackgroundProfileLocked returns a read-only view of the profile to use +// when no GUI/CLI client is connected. If Tailscale should not run in the background +// and should disconnect until a GUI/CLI client connects, the returned view is not valid. // As of 2025-02-07, it is only used on Windows. -func (b *LocalBackend) getBackgroundProfileLocked() (ipn.WindowsUserID, ipn.ProfileID) { +func (b *LocalBackend) getBackgroundProfileLocked() ipn.LoginProfileView { // TODO(nickkhyl): check if the returned profile is allowed on the device, // such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet. // See tailscale/corp#26249. // If Unattended Mode is enabled for the current profile, keep using it. if b.pm.CurrentPrefs().ForceDaemon() { - return b.pm.CurrentProfile().LocalUserID(), b.pm.CurrentProfile().ID() + return b.pm.CurrentProfile() } // Otherwise, attempt to resolve the background profile using the background // profile resolvers available on the current platform. for _, resolver := range b.backgroundProfileResolvers { - if uid, profileID, ok := resolver(); ok { - return uid, profileID + if profile := resolver(); profile.Valid() { + return profile } } // Otherwise, switch to an empty profile and disconnect Tailscale // until a GUI or CLI client connects. - return "", "" + return ipn.LoginProfileView{} } // CurrentUserForTest returns the current user and the associated WindowsUserID. @@ -7555,13 +7562,9 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error { unlock := b.lockAndGetUnlock() defer unlock() - if b.pm.CurrentProfile().ID() == profile { - return nil - } - oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault() - if err := b.pm.SwitchProfile(profile); err != nil { - return err + if _, changed, err := b.pm.SwitchToProfileByID(profile); !changed || err != nil { + return err // nil if we're already on the target profile } // As an optimization, only reset the dialPlan if the control URL changed. @@ -7750,7 +7753,7 @@ func (b *LocalBackend) NewProfile() error { unlock := b.lockAndGetUnlock() defer unlock() - b.pm.NewProfile() + b.pm.SwitchToNewProfile() // The new profile doesn't yet have a ControlURL because it hasn't been // set. Conservatively reset the dialPlan. diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 2579590a8..d29c2d4bb 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -4124,7 +4124,7 @@ func TestReadWriteRouteInfo(t *testing.T) { } // write the other routeInfo as the other profile - if err := b.pm.SwitchProfile("id2"); err != nil { + if _, _, err := b.pm.SwitchToProfileByID("id2"); err != nil { t.Fatal(err) } if err := b.storeRouteInfo(ri2); err != nil { @@ -4132,7 +4132,7 @@ func TestReadWriteRouteInfo(t *testing.T) { } // read the routeInfo of the first profile - if err := b.pm.SwitchProfile("id1"); err != nil { + if _, _, err := b.pm.SwitchToProfileByID("id1"); err != nil { t.Fatal(err) } readRi, err = b.readRouteInfoLocked() @@ -4144,7 +4144,7 @@ func TestReadWriteRouteInfo(t *testing.T) { } // read the routeInfo of the second profile - if err := b.pm.SwitchProfile("id2"); err != nil { + if _, _, err := b.pm.SwitchToProfileByID("id2"); err != nil { t.Fatal(err) } readRi, err = b.readRouteInfoLocked() diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index 10a110e61..901a4a899 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -64,8 +64,7 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) { if pm.currentUserID == uid { return } - pm.currentUserID = uid - if err := pm.SwitchToDefaultProfile(); err != nil { + if _, _, err := pm.SwitchToDefaultProfileForUser(uid); err != nil { // SetCurrentUserID should never fail and must always switch to the // user's default profile or create a new profile for the current user. // Until we implement multi-user support and the new permission model, @@ -73,79 +72,109 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) { // that when SetCurrentUserID exits, the profile in pm.currentProfile // is either an existing profile owned by the user, or a new, empty profile. pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err) - pm.NewProfileForUser(uid) + pm.SwitchToNewProfileForUser(uid) } } -// SetCurrentUserAndProfile sets the current user ID and switches the specified -// profile, if it is accessible to the user. If the profile does not exist, -// or is not accessible, it switches to the user's default profile, -// creating a new one if necessary. +// SwitchToProfile switches to the specified profile and (temporarily, +// while the "current user" is still a thing on Windows; see tailscale/corp#18342) +// sets its owner as the current user. The profile must be a valid profile +// returned by the [profileManager], such as by [profileManager.Profiles], +// [profileManager.ProfileByID], or [profileManager.NewProfileForUser]. // // It is a shorthand for [profileManager.SetCurrentUserID] followed by -// [profileManager.SwitchProfile], but it is more efficient as it switches +// [profileManager.SwitchProfileByID], but it is more efficient as it switches // directly to the specified profile rather than switching to the user's -// default profile first. +// default profile first. It is a no-op if the specified profile is already +// the current profile. // -// As a special case, if the specified profile ID "", it creates a new -// profile for the user and switches to it, unless the current profile -// is already a new, empty profile owned by the user. +// As a special case, if the specified profile view is not valid, it resets +// both the current user and the profile to a new, empty profile not owned +// by any user. // -// It returns the current profile and whether the call resulted -// in a profile switch. -func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (cp ipn.LoginProfileView, changed bool) { - pm.currentUserID = uid - - if profileID == "" { - if pm.currentProfile.ID() == "" && pm.currentProfile.LocalUserID() == uid { - return pm.currentProfile, false +// It returns the current profile and whether the call resulted in a profile change, +// or an error if the specified profile does not exist or its prefs could not be loaded. +func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.LoginProfileView, changed bool, err error) { + prefs := defaultPrefs + switch { + case !profile.Valid(): + // Create a new profile that is not associated with any user. + profile = pm.NewProfileForUser("") + case profile == pm.currentProfile, + profile.ID() != "" && profile.ID() == pm.currentProfile.ID(), + profile.ID() == "" && profile.Equals(pm.currentProfile) && prefs.Equals(pm.prefs): + // The profile is already the current profile; no need to switch. + // + // It includes three cases: + // 1. The target profile and the current profile are aliases referencing the [ipn.LoginProfile]. + // The profile may be either a new (non-persisted) profile or an existing well-known profile. + // 2. The target profile is a well-known, persisted profile with the same ID as the current profile. + // 3. The target and the current profiles are both new (non-persisted) profiles and they are equal. + // At minimum, equality means that the profiles are owned by the same user on platforms that support it + // and the prefs are the same as well. + return pm.currentProfile, false, nil + case profile.ID() == "": + // Copy the specified profile to prevent accidental mutation. + profile = profile.AsStruct().View() + default: + // Find an existing profile by ID and load its prefs. + kp, ok := pm.knownProfiles[profile.ID()] + if !ok { + // The profile ID is not valid; it may have been deleted or never existed. + // As the target profile should have been returned by the [profileManager], + // this is unexpected and might indicate a bug in the code. + return pm.currentProfile, false, fmt.Errorf("[unexpected] %w: %s (%s)", errProfileNotFound, profile.Name(), profile.ID()) + } + profile = kp + if prefs, err = pm.loadSavedPrefs(profile.Key()); err != nil { + return pm.currentProfile, false, fmt.Errorf("failed to load profile prefs for %s (%s): %w", profile.Name(), profile.ID(), err) } - pm.NewProfileForUser(uid) - return pm.currentProfile, true } - if profile, err := pm.ProfileByID(profileID); err == nil { - if pm.CurrentProfile().ID() == profileID { - return pm.currentProfile, false - } - if err := pm.SwitchProfile(profile.ID()); err == nil { - return pm.currentProfile, true - } + if profile.ID() == "" { // new profile that has never been persisted + metricNewProfile.Add(1) + } else { + metricSwitchProfile.Add(1) } - if err := pm.SwitchToDefaultProfile(); err != nil { - pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err) - pm.NewProfile() + pm.prefs = prefs + pm.updateHealth() + pm.currentProfile = profile + pm.currentUserID = profile.LocalUserID() + if err := pm.setProfileAsUserDefault(profile); err != nil { + // This is not a fatal error; we've already switched to the profile. + // But if updating the default profile fails, we should log it. + pm.logf("failed to set %s (%s) as the default profile: %v", profile.Name(), profile.ID(), err) } - return pm.currentProfile, true + return profile, true, nil } -// DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user, -// or an empty string if the specified user does not have a default profile. -func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.ProfileID { +// DefaultUserProfile returns a read-only view of the default (last used) profile for the specified user. +// It returns a read-only view of a new, non-persisted profile if the specified user does not have a default profile. +func (pm *profileManager) DefaultUserProfile(uid ipn.WindowsUserID) ipn.LoginProfileView { // Read the CurrentProfileKey from the store which stores // the selected profile for the specified user. b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid))) - pm.dlogf("DefaultUserProfileID: ReadState(%q) = %v, %v", string(uid), len(b), err) + pm.dlogf("DefaultUserProfile: ReadState(%q) = %v, %v", string(uid), len(b), err) if err == ipn.ErrStateNotExist || len(b) == 0 { if runtime.GOOS == "windows" { - pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences") + pm.dlogf("DefaultUserProfile: windows: migrating from legacy preferences") profile, err := pm.migrateFromLegacyPrefs(uid, false) if err == nil { - return profile.ID() + return profile } pm.logf("failed to migrate from legacy preferences: %v", err) } - return "" + return pm.NewProfileForUser(uid) } pk := ipn.StateKey(string(b)) prof := pm.findProfileByKey(uid, pk) if !prof.Valid() { - pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk) - return "" + pm.dlogf("DefaultUserProfile: no profile found for key: %q", pk) + return pm.NewProfileForUser(uid) } - return prof.ID() + return prof } // checkProfileAccess returns an [errProfileAccessDenied] if the current user @@ -251,12 +280,6 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error { } } -// Reset unloads the current profile, if any. -func (pm *profileManager) Reset() { - pm.currentUserID = "" - pm.NewProfile() -} - // SetPrefs sets the current profile's prefs to the provided value. // It also saves the prefs to the [ipn.StateStore]. It stores a copy of the // provided prefs, which may be accessed via [profileManager.CurrentPrefs]. @@ -477,42 +500,32 @@ func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, e return pm.loadSavedPrefs(p.Key()) } -// SwitchProfile switches to the profile with the given id. +// SwitchToProfileByID switches to the profile with the given id. +// It returns the current profile and whether the call resulted in a profile change. // If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied]. // If the profile does not exist, it returns an [errProfileNotFound]. -func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error { - metricSwitchProfile.Add(1) - - kp, ok := pm.knownProfiles[id] - if !ok { - return errProfileNotFound - } - if pm.currentProfile.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() { - return nil - } - - if err := pm.checkProfileAccess(kp); err != nil { - return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id) +func (pm *profileManager) SwitchToProfileByID(id ipn.ProfileID) (_ ipn.LoginProfileView, changed bool, err error) { + if id == pm.currentProfile.ID() { + return pm.currentProfile, false, nil } - prefs, err := pm.loadSavedPrefs(kp.Key()) + profile, err := pm.ProfileByID(id) if err != nil { - return err + return pm.currentProfile, false, err } - pm.prefs = prefs - pm.updateHealth() - pm.currentProfile = kp - return pm.setProfileAsUserDefault(kp) + return pm.SwitchToProfile(profile) } -// SwitchToDefaultProfile switches to the default (last used) profile for the current user. -// It creates a new one and switches to it if the current user does not have a default profile, +// SwitchToDefaultProfileForUser switches to the default (last used) profile for the specified user. +// It creates a new one and switches to it if the specified user does not have a default profile, // or returns an error if the default profile is inaccessible or could not be loaded. -func (pm *profileManager) SwitchToDefaultProfile() error { - if id := pm.DefaultUserProfileID(pm.currentUserID); id != "" { - return pm.SwitchProfile(id) - } - pm.NewProfileForUser(pm.currentUserID) - return nil +func (pm *profileManager) SwitchToDefaultProfileForUser(uid ipn.WindowsUserID) (_ ipn.LoginProfileView, changed bool, err error) { + return pm.SwitchToProfile(pm.DefaultUserProfile(uid)) +} + +// SwitchToDefaultProfile is like [profileManager.SwitchToDefaultProfileForUser], but switches +// to the default profile for the current user. +func (pm *profileManager) SwitchToDefaultProfile() (_ ipn.LoginProfileView, changed bool, err error) { + return pm.SwitchToDefaultProfileForUser(pm.currentUserID) } // setProfileAsUserDefault sets the specified profile as the default for the current user. @@ -610,7 +623,7 @@ func (pm *profileManager) deleteCurrentProfile() error { } if pm.currentProfile.ID() == "" { // Deleting the in-memory only new profile, just create a new one. - pm.NewProfile() + pm.SwitchToNewProfile() return nil } return pm.deleteProfileNoPermCheck(pm.currentProfile) @@ -620,7 +633,7 @@ func (pm *profileManager) deleteCurrentProfile() error { // but it doesn't check user's access rights to the profile. func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error { if profile.ID() == pm.currentProfile.ID() { - pm.NewProfile() + pm.SwitchToNewProfile() } if err := pm.WriteState(profile.Key(), nil); err != nil { return err @@ -637,7 +650,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error { currentProfileDeleted := false writeKnownProfiles := func() error { if currentProfileDeleted || pm.currentProfile.ID() == "" { - pm.NewProfile() + pm.SwitchToNewProfile() } return pm.writeKnownProfiles() } @@ -676,23 +689,22 @@ func (pm *profileManager) updateHealth() { pm.health.SetAutoUpdatePrefs(pm.prefs.AutoUpdate().Check, pm.prefs.AutoUpdate().Apply) } -// NewProfile creates and switches to a new unnamed profile. The new profile is +// SwitchToNewProfile creates and switches to a new unnamed profile. The new profile is // not persisted until [profileManager.SetPrefs] is called with a logged-in user. -func (pm *profileManager) NewProfile() { - pm.NewProfileForUser(pm.currentUserID) +func (pm *profileManager) SwitchToNewProfile() { + pm.SwitchToNewProfileForUser(pm.currentUserID) } -// NewProfileForUser is like [profileManager.NewProfile], but it switches to the +// SwitchToNewProfileForUser is like [profileManager.SwitchToNewProfile], but it switches to the // specified user and sets that user as the profile owner for the new profile. -func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) { - pm.currentUserID = uid - - metricNewProfile.Add(1) +func (pm *profileManager) SwitchToNewProfileForUser(uid ipn.WindowsUserID) { + pm.SwitchToProfile(pm.NewProfileForUser(uid)) +} - pm.prefs = defaultPrefs - pm.updateHealth() - newProfile := &ipn.LoginProfile{LocalUserID: uid} - pm.currentProfile = newProfile.View() +// NewProfileForUser creates a new profile for the specified user and returns a read-only view of it. +// It neither switches to the new profile nor persists it to the store. +func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) ipn.LoginProfileView { + return (&ipn.LoginProfile{LocalUserID: uid}).View() } // newProfileWithPrefs creates a new profile with the specified prefs and assigns @@ -816,7 +828,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok { pm.currentUserID = ipn.WindowsUserID(suf) } - pm.NewProfile() + pm.SwitchToNewProfile() } else { pm.currentUserID = pm.currentProfile.LocalUserID() } @@ -841,7 +853,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt return nil, err } } else { - pm.NewProfile() + pm.SwitchToNewProfile() } return pm, nil diff --git a/ipn/ipnlocal/profiles_test.go b/ipn/ipnlocal/profiles_test.go index 33209d24c..534951fb1 100644 --- a/ipn/ipnlocal/profiles_test.go +++ b/ipn/ipnlocal/profiles_test.go @@ -33,7 +33,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) { newProfile := func(t *testing.T, loginName string) ipn.PrefsView { id++ t.Helper() - pm.NewProfile() + pm.SwitchToNewProfile() p := pm.CurrentPrefs().AsStruct() p.Persist = &persist.Persist{ NodeID: tailcfg.StableNodeID(fmt.Sprint(id)), @@ -88,7 +88,7 @@ func TestProfileList(t *testing.T) { newProfile := func(t *testing.T, loginName string) ipn.PrefsView { id++ t.Helper() - pm.NewProfile() + pm.SwitchToNewProfile() p := pm.CurrentPrefs().AsStruct() p.Persist = &persist.Persist{ NodeID: tailcfg.StableNodeID(fmt.Sprint(id)), @@ -162,7 +162,7 @@ func TestProfileDupe(t *testing.T) { must.Do(pm.SetPrefs(prefs.View(), ipn.NetworkProfile{})) } login := func(pm *profileManager, p *persist.Persist) { - pm.NewProfile() + pm.SwitchToNewProfile() reauth(pm, p) } @@ -399,7 +399,7 @@ func TestProfileManagement(t *testing.T) { checkProfiles(t) t.Logf("Create new profile") - pm.NewProfile() + pm.SwitchToNewProfile() wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) @@ -438,7 +438,7 @@ func TestProfileManagement(t *testing.T) { checkProfiles(t) t.Logf("Create new profile - 2") - pm.NewProfile() + pm.SwitchToNewProfile() wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) @@ -550,7 +550,7 @@ func TestProfileManagementWindows(t *testing.T) { { t.Logf("Create new profile") - pm.NewProfile() + pm.SwitchToNewProfile() wantCurProfile = "" wantProfiles[""] = defaultPrefs checkProfiles(t) diff --git a/ipn/prefs.go b/ipn/prefs.go index 9d6008de1..1c9d71d73 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -593,7 +593,7 @@ func (p PrefsView) Equals(p2 PrefsView) bool { } func (p *Prefs) Equals(p2 *Prefs) bool { - if p == nil && p2 == nil { + if p == p2 { return true } if p == nil || p2 == nil { @@ -1014,3 +1014,26 @@ type LoginProfile struct { // into. ControlURL string } + +// Equals reports whether p and p2 are equal. +func (p LoginProfileView) Equals(p2 LoginProfileView) bool { + return p.ж.Equals(p2.ж) +} + +// Equals reports whether p and p2 are equal. +func (p *LoginProfile) Equals(p2 *LoginProfile) bool { + if p == p2 { + return true + } + if p == nil || p2 == nil { + return false + } + return p.ID == p2.ID && + p.Name == p2.Name && + p.NetworkProfile == p2.NetworkProfile && + p.Key == p2.Key && + p.UserProfile.Equal(&p2.UserProfile) && + p.NodeID == p2.NodeID && + p.LocalUserID == p2.LocalUserID && + p.ControlURL == p2.ControlURL +}