mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-28 14:53:44 +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:
|
// 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:
|
// 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.
|
// - 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
|
// 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
|
// when a locally signed-in user locks their screen or when a remote user
|
||||||
// disconnects without signing out.
|
// 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.
|
// 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()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
|
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
|
var foregroundUIDs []ipn.WindowsUserID
|
||||||
for _, s := range e.id2sess {
|
for _, s := range e.id2sess {
|
||||||
switch uid := s.User.UserID(); uid {
|
switch uid := s.User.UserID(); uid {
|
||||||
case e.pm.CurrentUserID():
|
case e.pm.CurrentProfile().LocalUserID():
|
||||||
isCurrentUserSingedIn = true
|
isCurrentProfileOwnerSignedIn = true
|
||||||
if s.Status == desktop.ForegroundSession {
|
if s.Status == desktop.ForegroundSession {
|
||||||
// Keep the current profile if the user has a foreground session.
|
// 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:
|
default:
|
||||||
if s.Status == desktop.ForegroundSession {
|
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
|
// If the current profile is empty and not owned by anyone (e.g., tailscaled just started),
|
||||||
// user has no foreground session, switch to the default profile of the first user
|
// or if the current profile's owner has no foreground session, switch to the default profile
|
||||||
// with a foreground session, if any.
|
// of the first user with a foreground session, if any.
|
||||||
for _, uid := range foregroundUIDs {
|
for _, uid := range foregroundUIDs {
|
||||||
if profileID := e.pm.DefaultUserProfileID(uid); profileID != "" {
|
if profile := e.pm.DefaultUserProfile(uid); profile.ID() != "" {
|
||||||
return uid, profileID, true
|
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,
|
// 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.
|
// such as when the screen is locked or a remote session is disconnected.
|
||||||
if len(foregroundUIDs) == 0 && isCurrentUserSingedIn {
|
if len(foregroundUIDs) == 0 && isCurrentProfileOwnerSignedIn {
|
||||||
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
return e.pm.CurrentProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", "", false
|
// Otherwise, there's no background profile.
|
||||||
|
return ipn.LoginProfileView{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown implements [localBackendExtension].
|
// Shutdown implements [localBackendExtension].
|
||||||
|
@ -204,13 +204,12 @@ func RegisterExtension(name string, newExt NewExtensionFn) {
|
|||||||
mak.Set(®isteredExtensions, name, newExt)
|
mak.Set(®isteredExtensions, name, newExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// profileResolver is any function that returns user and profile IDs
|
// profileResolver is any function that returns a read-only view of a login profile.
|
||||||
// along with a flag indicating whether it succeeded. Since an empty
|
// An invalid view indicates no profile. A valid profile view with an empty [ipn.ProfileID]
|
||||||
// profile ID ("") represents an empty profile, the ok return parameter
|
// indicates that the profile is new and has not been persisted yet.
|
||||||
// distinguishes between an empty profile and no profile.
|
|
||||||
//
|
//
|
||||||
// It is called with [LocalBackend.mu] held.
|
// 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]
|
// 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
|
// 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) {
|
func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock unlockOnce) {
|
||||||
defer unlock()
|
defer unlock()
|
||||||
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
||||||
uid, profileID, background := b.resolveBestProfileLocked()
|
profile, background := b.resolveBestProfileLocked()
|
||||||
cp, switched := b.pm.SetCurrentUserAndProfile(uid, profileID)
|
cp, switched, err := b.pm.SwitchToProfile(profile)
|
||||||
switch {
|
switch {
|
||||||
case !switched && cp.ID() == "":
|
case !switched && cp.ID() == "":
|
||||||
|
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)
|
b.logf("%s: staying on empty profile", reason)
|
||||||
|
}
|
||||||
case !switched:
|
case !switched:
|
||||||
|
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())
|
b.logf("%s: staying on profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID())
|
||||||
|
}
|
||||||
case cp.ID() == "":
|
case cp.ID() == "":
|
||||||
b.logf("%s: disconnecting Tailscale", reason)
|
b.logf("%s: disconnecting Tailscale", reason)
|
||||||
case background:
|
case background:
|
||||||
@ -4032,7 +4039,7 @@ func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock un
|
|||||||
// the TKA initialization or [LocalBackend.Start] can fail.
|
// the TKA initialization or [LocalBackend.Start] can fail.
|
||||||
// These errors are not critical as far as we're concerned.
|
// These errors are not critical as far as we're concerned.
|
||||||
// But maybe we should post a notification to the API watchers?
|
// 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,
|
// the unattended mode is enabled, the current state of the desktop sessions,
|
||||||
// and other factors.
|
// and other factors.
|
||||||
//
|
//
|
||||||
// It returns the user ID, profile ID, and whether the returned profile is
|
// It returns a read-only view of the profile and whether it is considered
|
||||||
// considered a background profile. A background profile is used when no OS user
|
// a background profile. A background profile is used when no OS user is actively
|
||||||
// is actively using Tailscale, such as when no GUI/CLI client is connected
|
// using Tailscale, such as when no GUI/CLI client is connected and Unattended Mode
|
||||||
// and Unattended Mode is enabled (see also [LocalBackend.getBackgroundProfileLocked]).
|
// is enabled (see also [LocalBackend.getBackgroundProfileLocked]).
|
||||||
// An empty profile ID indicates that Tailscale should switch to an empty profile.
|
//
|
||||||
|
// 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.
|
// 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
|
// 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.
|
// either the current profile if owned by the user, or their default profile.
|
||||||
if b.currentUser != nil {
|
if b.currentUser != nil {
|
||||||
cp := b.pm.CurrentProfile()
|
profile := b.pm.CurrentProfile()
|
||||||
uid := b.currentUser.UserID()
|
|
||||||
|
|
||||||
var profileID ipn.ProfileID
|
|
||||||
// TODO(nickkhyl): check if the current profile is allowed on the device,
|
// TODO(nickkhyl): check if the current profile is allowed on the device,
|
||||||
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
||||||
// See tailscale/corp#26249.
|
// See tailscale/corp#26249.
|
||||||
if cp.LocalUserID() == uid {
|
if uid := b.currentUser.UserID(); profile.LocalUserID() != uid {
|
||||||
profileID = cp.ID()
|
profile = b.pm.DefaultUserProfile(uid)
|
||||||
} else {
|
|
||||||
profileID = b.pm.DefaultUserProfileID(uid)
|
|
||||||
}
|
}
|
||||||
return uid, profileID, false
|
return profile, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, if on Windows, use the background profile if one is set.
|
// 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
|
// If the returned background profileID is "", Tailscale will disconnect
|
||||||
// and remain idle until a GUI or CLI client connects.
|
// and remain idle until a GUI or CLI client connects.
|
||||||
if goos := envknob.GOOS(); goos == "windows" {
|
if goos := envknob.GOOS(); goos == "windows" {
|
||||||
uid, profileID := b.getBackgroundProfileLocked()
|
profile := b.getBackgroundProfileLocked()
|
||||||
return uid, profileID, true
|
return profile, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// On other platforms, however, Tailscale continues to run in the background
|
// 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,
|
// TODO(nickkhyl): check if the current profile is allowed on the device,
|
||||||
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
||||||
// See tailscale/corp#26249.
|
// 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
|
// 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
|
// getBackgroundProfileLocked returns a read-only view of the profile to use
|
||||||
// client is connected, or "","" if Tailscale should not run in the background.
|
// 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.
|
// 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,
|
// TODO(nickkhyl): check if the returned profile is allowed on the device,
|
||||||
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
||||||
// See tailscale/corp#26249.
|
// See tailscale/corp#26249.
|
||||||
|
|
||||||
// If Unattended Mode is enabled for the current profile, keep using it.
|
// If Unattended Mode is enabled for the current profile, keep using it.
|
||||||
if b.pm.CurrentPrefs().ForceDaemon() {
|
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
|
// Otherwise, attempt to resolve the background profile using the background
|
||||||
// profile resolvers available on the current platform.
|
// profile resolvers available on the current platform.
|
||||||
for _, resolver := range b.backgroundProfileResolvers {
|
for _, resolver := range b.backgroundProfileResolvers {
|
||||||
if uid, profileID, ok := resolver(); ok {
|
if profile := resolver(); profile.Valid() {
|
||||||
return uid, profileID
|
return profile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, switch to an empty profile and disconnect Tailscale
|
// Otherwise, switch to an empty profile and disconnect Tailscale
|
||||||
// until a GUI or CLI client connects.
|
// until a GUI or CLI client connects.
|
||||||
return "", ""
|
return ipn.LoginProfileView{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentUserForTest returns the current user and the associated WindowsUserID.
|
// CurrentUserForTest returns the current user and the associated WindowsUserID.
|
||||||
@ -7555,13 +7562,9 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
|
|||||||
unlock := b.lockAndGetUnlock()
|
unlock := b.lockAndGetUnlock()
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
if b.pm.CurrentProfile().ID() == profile {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
||||||
if err := b.pm.SwitchProfile(profile); err != nil {
|
if _, changed, err := b.pm.SwitchToProfileByID(profile); !changed || err != nil {
|
||||||
return err
|
return err // nil if we're already on the target profile
|
||||||
}
|
}
|
||||||
|
|
||||||
// As an optimization, only reset the dialPlan if the control URL changed.
|
// As an optimization, only reset the dialPlan if the control URL changed.
|
||||||
@ -7750,7 +7753,7 @@ func (b *LocalBackend) NewProfile() error {
|
|||||||
unlock := b.lockAndGetUnlock()
|
unlock := b.lockAndGetUnlock()
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
b.pm.NewProfile()
|
b.pm.SwitchToNewProfile()
|
||||||
|
|
||||||
// The new profile doesn't yet have a ControlURL because it hasn't been
|
// The new profile doesn't yet have a ControlURL because it hasn't been
|
||||||
// set. Conservatively reset the dialPlan.
|
// set. Conservatively reset the dialPlan.
|
||||||
|
@ -4124,7 +4124,7 @@ func TestReadWriteRouteInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write the other routeInfo as the other profile
|
// 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := b.storeRouteInfo(ri2); err != nil {
|
if err := b.storeRouteInfo(ri2); err != nil {
|
||||||
@ -4132,7 +4132,7 @@ func TestReadWriteRouteInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// read the routeInfo of the first profile
|
// 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
readRi, err = b.readRouteInfoLocked()
|
readRi, err = b.readRouteInfoLocked()
|
||||||
@ -4144,7 +4144,7 @@ func TestReadWriteRouteInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// read the routeInfo of the second profile
|
// 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
readRi, err = b.readRouteInfoLocked()
|
readRi, err = b.readRouteInfoLocked()
|
||||||
|
@ -64,8 +64,7 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
|
|||||||
if pm.currentUserID == uid {
|
if pm.currentUserID == uid {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pm.currentUserID = uid
|
if _, _, err := pm.SwitchToDefaultProfileForUser(uid); err != nil {
|
||||||
if err := pm.SwitchToDefaultProfile(); err != nil {
|
|
||||||
// SetCurrentUserID should never fail and must always switch to the
|
// SetCurrentUserID should never fail and must always switch to the
|
||||||
// user's default profile or create a new profile for the current user.
|
// user's default profile or create a new profile for the current user.
|
||||||
// Until we implement multi-user support and the new permission model,
|
// 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
|
// that when SetCurrentUserID exits, the profile in pm.currentProfile
|
||||||
// is either an existing profile owned by the user, or a new, empty profile.
|
// 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.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
|
// SwitchToProfile switches to the specified profile and (temporarily,
|
||||||
// profile, if it is accessible to the user. If the profile does not exist,
|
// while the "current user" is still a thing on Windows; see tailscale/corp#18342)
|
||||||
// or is not accessible, it switches to the user's default profile,
|
// sets its owner as the current user. The profile must be a valid profile
|
||||||
// creating a new one if necessary.
|
// returned by the [profileManager], such as by [profileManager.Profiles],
|
||||||
|
// [profileManager.ProfileByID], or [profileManager.NewProfileForUser].
|
||||||
//
|
//
|
||||||
// It is a shorthand for [profileManager.SetCurrentUserID] followed by
|
// 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
|
// 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
|
// As a special case, if the specified profile view is not valid, it resets
|
||||||
// profile for the user and switches to it, unless the current profile
|
// both the current user and the profile to a new, empty profile not owned
|
||||||
// is already a new, empty profile owned by the user.
|
// by any user.
|
||||||
//
|
//
|
||||||
// It returns the current profile and whether the call resulted
|
// It returns the current profile and whether the call resulted in a profile change,
|
||||||
// in a profile switch.
|
// or an error if the specified profile does not exist or its prefs could not be loaded.
|
||||||
func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (cp ipn.LoginProfileView, changed bool) {
|
func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.LoginProfileView, changed bool, err error) {
|
||||||
pm.currentUserID = uid
|
prefs := defaultPrefs
|
||||||
|
switch {
|
||||||
if profileID == "" {
|
case !profile.Valid():
|
||||||
if pm.currentProfile.ID() == "" && pm.currentProfile.LocalUserID() == uid {
|
// Create a new profile that is not associated with any user.
|
||||||
return pm.currentProfile, false
|
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)
|
profile = kp
|
||||||
return pm.currentProfile, true
|
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 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 err := pm.SwitchToDefaultProfile(); err != nil {
|
if profile.ID() == "" { // new profile that has never been persisted
|
||||||
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
|
metricNewProfile.Add(1)
|
||||||
pm.NewProfile()
|
} else {
|
||||||
}
|
metricSwitchProfile.Add(1)
|
||||||
return pm.currentProfile, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user,
|
pm.prefs = prefs
|
||||||
// or an empty string if the specified user does not have a default profile.
|
pm.updateHealth()
|
||||||
func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.ProfileID {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Read the CurrentProfileKey from the store which stores
|
||||||
// the selected profile for the specified user.
|
// the selected profile for the specified user.
|
||||||
b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid)))
|
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 err == ipn.ErrStateNotExist || len(b) == 0 {
|
||||||
if runtime.GOOS == "windows" {
|
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)
|
profile, err := pm.migrateFromLegacyPrefs(uid, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return profile.ID()
|
return profile
|
||||||
}
|
}
|
||||||
pm.logf("failed to migrate from legacy preferences: %v", err)
|
pm.logf("failed to migrate from legacy preferences: %v", err)
|
||||||
}
|
}
|
||||||
return ""
|
return pm.NewProfileForUser(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pk := ipn.StateKey(string(b))
|
pk := ipn.StateKey(string(b))
|
||||||
prof := pm.findProfileByKey(uid, pk)
|
prof := pm.findProfileByKey(uid, pk)
|
||||||
if !prof.Valid() {
|
if !prof.Valid() {
|
||||||
pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
|
pm.dlogf("DefaultUserProfile: no profile found for key: %q", pk)
|
||||||
return ""
|
return pm.NewProfileForUser(uid)
|
||||||
}
|
}
|
||||||
return prof.ID()
|
return prof
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkProfileAccess returns an [errProfileAccessDenied] if the current user
|
// 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.
|
// 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
|
// It also saves the prefs to the [ipn.StateStore]. It stores a copy of the
|
||||||
// provided prefs, which may be accessed via [profileManager.CurrentPrefs].
|
// 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())
|
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 exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
|
||||||
// If the profile does not exist, it returns an [errProfileNotFound].
|
// If the profile does not exist, it returns an [errProfileNotFound].
|
||||||
func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
|
func (pm *profileManager) SwitchToProfileByID(id ipn.ProfileID) (_ ipn.LoginProfileView, changed bool, err error) {
|
||||||
metricSwitchProfile.Add(1)
|
if id == pm.currentProfile.ID() {
|
||||||
|
return pm.currentProfile, false, nil
|
||||||
kp, ok := pm.knownProfiles[id]
|
|
||||||
if !ok {
|
|
||||||
return errProfileNotFound
|
|
||||||
}
|
}
|
||||||
if pm.currentProfile.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() {
|
profile, err := pm.ProfileByID(id)
|
||||||
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())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return pm.currentProfile, false, err
|
||||||
}
|
}
|
||||||
pm.prefs = prefs
|
return pm.SwitchToProfile(profile)
|
||||||
pm.updateHealth()
|
|
||||||
pm.currentProfile = kp
|
|
||||||
return pm.setProfileAsUserDefault(kp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwitchToDefaultProfile switches to the default (last used) profile for the current user.
|
// SwitchToDefaultProfileForUser switches to the default (last used) profile for the specified user.
|
||||||
// It creates a new one and switches to it if the current user does not have a default profile,
|
// 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.
|
// or returns an error if the default profile is inaccessible or could not be loaded.
|
||||||
func (pm *profileManager) SwitchToDefaultProfile() error {
|
func (pm *profileManager) SwitchToDefaultProfileForUser(uid ipn.WindowsUserID) (_ ipn.LoginProfileView, changed bool, err error) {
|
||||||
if id := pm.DefaultUserProfileID(pm.currentUserID); id != "" {
|
return pm.SwitchToProfile(pm.DefaultUserProfile(uid))
|
||||||
return pm.SwitchProfile(id)
|
|
||||||
}
|
}
|
||||||
pm.NewProfileForUser(pm.currentUserID)
|
|
||||||
return nil
|
// 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.
|
// 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() == "" {
|
if pm.currentProfile.ID() == "" {
|
||||||
// Deleting the in-memory only new profile, just create a new one.
|
// Deleting the in-memory only new profile, just create a new one.
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return pm.deleteProfileNoPermCheck(pm.currentProfile)
|
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.
|
// but it doesn't check user's access rights to the profile.
|
||||||
func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
|
func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
|
||||||
if profile.ID() == pm.currentProfile.ID() {
|
if profile.ID() == pm.currentProfile.ID() {
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
}
|
}
|
||||||
if err := pm.WriteState(profile.Key(), nil); err != nil {
|
if err := pm.WriteState(profile.Key(), nil); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -637,7 +650,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
|
|||||||
currentProfileDeleted := false
|
currentProfileDeleted := false
|
||||||
writeKnownProfiles := func() error {
|
writeKnownProfiles := func() error {
|
||||||
if currentProfileDeleted || pm.currentProfile.ID() == "" {
|
if currentProfileDeleted || pm.currentProfile.ID() == "" {
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
}
|
}
|
||||||
return pm.writeKnownProfiles()
|
return pm.writeKnownProfiles()
|
||||||
}
|
}
|
||||||
@ -676,23 +689,22 @@ func (pm *profileManager) updateHealth() {
|
|||||||
pm.health.SetAutoUpdatePrefs(pm.prefs.AutoUpdate().Check, pm.prefs.AutoUpdate().Apply)
|
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.
|
// not persisted until [profileManager.SetPrefs] is called with a logged-in user.
|
||||||
func (pm *profileManager) NewProfile() {
|
func (pm *profileManager) SwitchToNewProfile() {
|
||||||
pm.NewProfileForUser(pm.currentUserID)
|
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.
|
// specified user and sets that user as the profile owner for the new profile.
|
||||||
func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
|
func (pm *profileManager) SwitchToNewProfileForUser(uid ipn.WindowsUserID) {
|
||||||
pm.currentUserID = uid
|
pm.SwitchToProfile(pm.NewProfileForUser(uid))
|
||||||
|
}
|
||||||
|
|
||||||
metricNewProfile.Add(1)
|
// 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.
|
||||||
pm.prefs = defaultPrefs
|
func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) ipn.LoginProfileView {
|
||||||
pm.updateHealth()
|
return (&ipn.LoginProfile{LocalUserID: uid}).View()
|
||||||
newProfile := &ipn.LoginProfile{LocalUserID: uid}
|
|
||||||
pm.currentProfile = newProfile.View()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newProfileWithPrefs creates a new profile with the specified prefs and assigns
|
// 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 {
|
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
|
||||||
pm.currentUserID = ipn.WindowsUserID(suf)
|
pm.currentUserID = ipn.WindowsUserID(suf)
|
||||||
}
|
}
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
} else {
|
} else {
|
||||||
pm.currentUserID = pm.currentProfile.LocalUserID()
|
pm.currentUserID = pm.currentProfile.LocalUserID()
|
||||||
}
|
}
|
||||||
@ -841,7 +853,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
return pm, nil
|
return pm, nil
|
||||||
|
@ -33,7 +33,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
|
|||||||
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
||||||
id++
|
id++
|
||||||
t.Helper()
|
t.Helper()
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
p := pm.CurrentPrefs().AsStruct()
|
p := pm.CurrentPrefs().AsStruct()
|
||||||
p.Persist = &persist.Persist{
|
p.Persist = &persist.Persist{
|
||||||
NodeID: tailcfg.StableNodeID(fmt.Sprint(id)),
|
NodeID: tailcfg.StableNodeID(fmt.Sprint(id)),
|
||||||
@ -88,7 +88,7 @@ func TestProfileList(t *testing.T) {
|
|||||||
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
||||||
id++
|
id++
|
||||||
t.Helper()
|
t.Helper()
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
p := pm.CurrentPrefs().AsStruct()
|
p := pm.CurrentPrefs().AsStruct()
|
||||||
p.Persist = &persist.Persist{
|
p.Persist = &persist.Persist{
|
||||||
NodeID: tailcfg.StableNodeID(fmt.Sprint(id)),
|
NodeID: tailcfg.StableNodeID(fmt.Sprint(id)),
|
||||||
@ -162,7 +162,7 @@ func TestProfileDupe(t *testing.T) {
|
|||||||
must.Do(pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}))
|
must.Do(pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}))
|
||||||
}
|
}
|
||||||
login := func(pm *profileManager, p *persist.Persist) {
|
login := func(pm *profileManager, p *persist.Persist) {
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
reauth(pm, p)
|
reauth(pm, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,7 +399,7 @@ func TestProfileManagement(t *testing.T) {
|
|||||||
checkProfiles(t)
|
checkProfiles(t)
|
||||||
|
|
||||||
t.Logf("Create new profile")
|
t.Logf("Create new profile")
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
wantCurProfile = ""
|
wantCurProfile = ""
|
||||||
wantProfiles[""] = defaultPrefs
|
wantProfiles[""] = defaultPrefs
|
||||||
checkProfiles(t)
|
checkProfiles(t)
|
||||||
@ -438,7 +438,7 @@ func TestProfileManagement(t *testing.T) {
|
|||||||
checkProfiles(t)
|
checkProfiles(t)
|
||||||
|
|
||||||
t.Logf("Create new profile - 2")
|
t.Logf("Create new profile - 2")
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
wantCurProfile = ""
|
wantCurProfile = ""
|
||||||
wantProfiles[""] = defaultPrefs
|
wantProfiles[""] = defaultPrefs
|
||||||
checkProfiles(t)
|
checkProfiles(t)
|
||||||
@ -550,7 +550,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
t.Logf("Create new profile")
|
t.Logf("Create new profile")
|
||||||
pm.NewProfile()
|
pm.SwitchToNewProfile()
|
||||||
wantCurProfile = ""
|
wantCurProfile = ""
|
||||||
wantProfiles[""] = defaultPrefs
|
wantProfiles[""] = defaultPrefs
|
||||||
checkProfiles(t)
|
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 {
|
func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||||
if p == nil && p2 == nil {
|
if p == p2 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if p == nil || p2 == nil {
|
if p == nil || p2 == nil {
|
||||||
@ -1014,3 +1014,26 @@ type LoginProfile struct {
|
|||||||
// into.
|
// into.
|
||||||
ControlURL string
|
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