mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-15 15:08:36 +00:00
ipn, ipn/ipnlocal: reduce coupling between LocalBackend/profileManager and the Windows-specific "current user" model
Ultimately, we'd like to get rid of the concept of the "current user". It is only used on Windows, but even then it doesn't work well in multi-user and enterprise/managed Windows environments. In this PR, we update LocalBackend and profileManager to decouple them a bit more from this obsolete concept. This is done in a preparation for extracting ipnlocal.Extension-related interfaces and types, and using them to implement optional features like tailscale/corp#27645, instead of continuing growing the core ipnlocal logic. Notably, we rename (*profileManager).SetCurrentUserAndProfile() to SwitchToProfile() and change its signature to accept an ipn.LoginProfileView instead of an ipn.ProfileID and ipn.WindowsUserID. Since we're not removing the "current user" completely just yet, the method sets the current user to the owner of the target profile. We also update the profileResolver callback type, which is typically implemented by LocalBackend extensions, to return an ipn.LoginProfileView instead of ipn.ProfileID and ipn.WindowsUserID. Updates tailscale/corp#27645 Updates tailscale/corp#18342 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
476a4c6ff1
commit
94f4f83731
@ -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].
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := pm.SwitchToDefaultProfile(); err != nil {
|
||||
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
|
||||
pm.NewProfile()
|
||||
if profile.ID() == "" { // new profile that has never been persisted
|
||||
metricNewProfile.Add(1)
|
||||
} else {
|
||||
metricSwitchProfile.Add(1)
|
||||
}
|
||||
return pm.currentProfile, true
|
||||
|
||||
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 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
|
||||
func (pm *profileManager) SwitchToProfileByID(id ipn.ProfileID) (_ ipn.LoginProfileView, changed bool, err error) {
|
||||
if id == pm.currentProfile.ID() {
|
||||
return pm.currentProfile, false, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
func (pm *profileManager) SwitchToNewProfileForUser(uid ipn.WindowsUserID) {
|
||||
pm.SwitchToProfile(pm.NewProfileForUser(uid))
|
||||
}
|
||||
|
||||
metricNewProfile.Add(1)
|
||||
|
||||
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
|
||||
|
@ -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)
|
||||
|
25
ipn/prefs.go
25
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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user