diff --git a/ipn/ipnlocal/drive.go b/ipn/ipnlocal/drive.go index c5380d9af..5dcd1af37 100644 --- a/ipn/ipnlocal/drive.go +++ b/ipn/ipnlocal/drive.go @@ -243,7 +243,7 @@ func (b *LocalBackend) driveSetSharesLocked(shares []*drive.Share) error { }, DriveSharesSet: true, }) - return b.pm.setPrefsLocked(prefs.View()) + return b.pm.setPrefsNoPermCheck(prefs.View()) } // driveNotifyShares notifies IPN bus listeners (e.g. Mac Application process) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b57be7a05..c6486c503 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3321,9 +3321,7 @@ func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) (ipn.WindowsUserID, e if b.pm.CurrentUserID() == uid { return uid, nil } - if err := b.pm.SetCurrentUserID(uid); err != nil { - return uid, nil - } + b.pm.SetCurrentUserID(uid) if c, ok := b.currentUser.(ipnauth.ActorCloser); ok { c.Close() } @@ -6575,7 +6573,7 @@ func (b *LocalBackend) ResetAuth() error { if err := b.clearMachineKeyLocked(); err != nil { return err } - if err := b.pm.DeleteAllProfiles(); err != nil { + if err := b.pm.DeleteAllProfilesForUser(); err != nil { return err } b.resetDialPlan() // always reset if we're removing everything diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index b5c22e54b..bf4a28ff9 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -2654,7 +2654,7 @@ func TestOnTailnetDefaultAutoUpdate(t *testing.T) { b.hostinfo.Container = tt.container p := ipn.NewPrefs() p.AutoUpdate.Apply = tt.before - if err := b.pm.setPrefsLocked(p.View()); err != nil { + if err := b.pm.setPrefsNoPermCheck(p.View()); err != nil { t.Fatal(err) } b.onTailnetDefaultAutoUpdate(tt.tailnetDefault) diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index 05286665e..b13f921d6 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -17,19 +17,19 @@ import ( "tailscale.com/envknob" "tailscale.com/health" "tailscale.com/ipn" + "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/util/clientmetric" ) -var errAlreadyMigrated = errors.New("profile migration already completed") - var debug = envknob.RegisterBool("TS_DEBUG_PROFILES") -// profileManager is a wrapper around a StateStore that manages +// profileManager is a wrapper around an [ipn.StateStore] that manages // multiple profiles and the current profile. // // It is not safe for concurrent use. type profileManager struct { + goos string // used for TestProfileManagementWindows store ipn.StateStore logf logger.Logf health *health.Tracker @@ -57,61 +57,68 @@ func (pm *profileManager) CurrentUserID() ipn.WindowsUserID { return pm.currentUserID } -// SetCurrentUserID sets the current user ID. The uid is only non-empty -// on Windows where we have a multi-user system. -func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error { +// SetCurrentUserID sets the current user ID and switches to that user's default (last used) profile. +// If the specified user does not have a default profile, or the default profile could not be loaded, +// it creates a new one and switches to it. The uid is only non-empty on Windows where we have a multi-user system. +func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) { if pm.currentUserID == uid { - return nil + return } - prev := pm.currentUserID pm.currentUserID = uid - if uid == "" && prev != "" { - // This is a local user logout, or app shutdown. - // Clear the current profile. - pm.NewProfile() - return nil + if err := pm.SwitchToDefaultProfile(); 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, + // and remove the concept of the "current user" completely, we must ensure + // 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) } +} +// 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 { // Read the CurrentProfileKey from the store which stores - // the selected profile for the current user. + // the selected profile for the specified user. b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid))) - pm.dlogf("SetCurrentUserID: ReadState(%q) = %v, %v", string(uid), len(b), err) + pm.dlogf("DefaultUserProfileID: ReadState(%q) = %v, %v", string(uid), len(b), err) if err == ipn.ErrStateNotExist || len(b) == 0 { if runtime.GOOS == "windows" { - pm.dlogf("SetCurrentUserID: windows: migrating from legacy preferences") - if err := pm.migrateFromLegacyPrefs(); err != nil && !errors.Is(err, errAlreadyMigrated) { - return err + pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences") + profile, err := pm.migrateFromLegacyPrefs(uid, false) + if err == nil { + return profile.ID } - } else { - pm.NewProfile() + pm.logf("failed to migrate from legacy preferences: %v", err) } - return nil + return "" } - // Now attempt to load the profile using the key we just read. pk := ipn.StateKey(string(b)) prof := pm.findProfileByKey(pk) if prof == nil { - pm.dlogf("SetCurrentUserID: no profile found for key: %q", pk) - pm.NewProfile() - return nil + pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk) + return "" } - prefs, err := pm.loadSavedPrefs(pk) - if err != nil { - pm.NewProfile() - return err + return prof.ID +} + +// checkProfileAccess returns an [errProfileAccessDenied] if the current user +// does not have access to the specified profile. +func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error { + if pm.currentUserID != "" && profile.LocalUserID != pm.currentUserID { + return errProfileAccessDenied } - pm.currentProfile = prof - pm.prefs = prefs - pm.updateHealth() return nil } -// allProfiles returns all profiles that belong to the currentUserID. +// allProfiles returns all profiles accessible to the current user. // The returned profiles are sorted by Name. func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) { for _, p := range pm.knownProfiles { - if p.LocalUserID == pm.currentUserID { + if pm.checkProfileAccess(p) == nil { out = append(out, p) } } @@ -121,9 +128,8 @@ func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) { return out } -// matchingProfiles returns all profiles that match the given predicate and -// belong to the currentUserID. -// The returned profiles are sorted by Name. +// matchingProfiles is like [profileManager.allProfiles], but returns only profiles +// matching the given predicate. func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) { all := pm.allProfiles() out = all[:0] @@ -135,19 +141,20 @@ func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out return out } -// findMatchinProfiles returns all profiles that represent the same node/user as -// prefs. +// findMatchingProfiles returns all profiles accessible to the current user +// that represent the same node/user as prefs. // The returned profiles are sorted by Name. -func (pm *profileManager) findMatchingProfiles(prefs *ipn.Prefs) []*ipn.LoginProfile { +func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.LoginProfile { return pm.matchingProfiles(func(p *ipn.LoginProfile) bool { - return p.ControlURL == prefs.ControlURL && - (p.UserProfile.ID == prefs.Persist.UserProfile.ID || - p.NodeID == prefs.Persist.NodeID) + return p.ControlURL == prefs.ControlURL() && + (p.UserProfile.ID == prefs.Persist().UserProfile().ID || + p.NodeID == prefs.Persist().NodeID()) }) } // ProfileIDForName returns the profile ID for the profile with the -// given name. It returns "" if no such profile exists. +// given name. It returns "" if no such profile exists among profiles +// accessible to the current user. func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID { p := pm.findProfileByName(name) if p == nil { @@ -164,7 +171,7 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile { return nil } if len(out) > 1 { - pm.logf("[unxpected] multiple profiles with the same name") + pm.logf("[unexpected] multiple profiles with the same name") } return out[0] } @@ -177,17 +184,17 @@ func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile { return nil } if len(out) > 1 { - pm.logf("[unxpected] multiple profiles with the same key") + pm.logf("[unexpected] multiple profiles with the same key") } return out[0] } func (pm *profileManager) setUnattendedModeAsConfigured() error { - if pm.currentUserID == "" { + if pm.goos != "windows" { return nil } - if pm.prefs.ForceDaemon() { + if pm.currentProfile.Key != "" && pm.prefs.ForceDaemon() { return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key)) } else { return pm.WriteState(ipn.ServerModeStartKey, nil) @@ -201,26 +208,21 @@ func (pm *profileManager) Reset() { } // SetPrefs sets the current profile's prefs to the provided value. -// It also saves the prefs to the StateStore. It stores a copy of the -// provided prefs, which may be accessed via CurrentPrefs. +// It also saves the prefs to the [ipn.StateStore]. It stores a copy of the +// provided prefs, which may be accessed via [profileManager.CurrentPrefs]. // -// NetworkProfile stores additional information about the tailnet the user +// The [ipn.NetworkProfile] stores additional information about the tailnet the user // is logged into so that we can keep track of things like their domain name // across user switches to disambiguate the same account but a different tailnet. func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error { - prefs := prefsIn.AsStruct() - newPersist := prefs.Persist - if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" { - // We don't know anything about this profile, so ignore it for now. - return pm.setPrefsLocked(prefs.View()) - } - up := newPersist.UserProfile - if up.DisplayName == "" { - up.DisplayName = up.LoginName - } cp := pm.currentProfile + if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName == "" { + // We don't know anything about this profile, so ignore it for now. + return pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefsIn.AsStruct().View()) + } + // Check if we already have an existing profile that matches the user/node. - if existing := pm.findMatchingProfiles(prefs); len(existing) > 0 { + if existing := pm.findMatchingProfiles(prefsIn); len(existing) > 0 { // We already have a profile for this user/node we should reuse it. Also // cleanup any other duplicate profiles. cp = existing[0] @@ -231,37 +233,76 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) // We couldn't delete the state, so keep the profile around. continue } - // Remove the profile, knownProfiles will be persisted below. + // Remove the profile, knownProfiles will be persisted + // in [profileManager.setProfilePrefs] below. delete(pm.knownProfiles, p.ID) } - } else if cp.ID == "" { - // We didn't have an existing profile, so create a new one. - cp.ID, cp.Key = newUnusedID(pm.knownProfiles) - cp.LocalUserID = pm.currentUserID - } else { - // This means that there was a force-reauth as a new node that - // we haven't seen before. + } + pm.currentProfile = cp + if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil { + return err + } + return pm.setProfileAsUserDefault(cp) + +} + +// SetProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile] +// which is not necessarily the [profileManager.CurrentProfile]. It returns an [errProfileAccessDenied] +// if the specified profile is not accessible by the current user. +func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) error { + if err := pm.checkProfileAccess(lp); err != nil { + return err } - if prefs.ProfileName != "" { - cp.Name = prefs.ProfileName + // An empty profile.ID indicates that the profile is new, the node info wasn't available, + // and it hasn't been persisted yet. We'll generate both an ID and [ipn.StateKey] + // once the information is available and needs to be persisted. + if lp.ID == "" { + if persist := prefsIn.Persist(); persist.Valid() && persist.NodeID() != "" && persist.UserProfile().LoginName != "" { + // Generate an ID and [ipn.StateKey] now that we have the node info. + lp.ID, lp.Key = newUnusedID(pm.knownProfiles) + } + + // Set the current user as the profile owner, unless the current user ID does + // not represent a specific user, or the profile is already owned by a different user. + // It is only relevant on Windows where we have a multi-user system. + if lp.LocalUserID == "" && pm.currentUserID != "" { + lp.LocalUserID = pm.currentUserID + } + } + + var up tailcfg.UserProfile + if persist := prefsIn.Persist(); persist.Valid() { + up = persist.UserProfile() + if up.DisplayName == "" { + up.DisplayName = up.LoginName + } + lp.NodeID = persist.NodeID() } else { - cp.Name = up.LoginName + lp.NodeID = "" } - cp.ControlURL = prefs.ControlURL - cp.UserProfile = newPersist.UserProfile - cp.NodeID = newPersist.NodeID - cp.NetworkProfile = np - pm.knownProfiles[cp.ID] = cp - pm.currentProfile = cp - if err := pm.writeKnownProfiles(); err != nil { - return err + + if prefsIn.ProfileName() != "" { + lp.Name = prefsIn.ProfileName() + } else { + lp.Name = up.LoginName } - if err := pm.setAsUserSelectedProfileLocked(); err != nil { - return err - } - if err := pm.setPrefsLocked(prefs.View()); err != nil { - return err + lp.ControlURL = prefsIn.ControlURL() + lp.UserProfile = up + lp.NetworkProfile = np + + // An empty profile.ID indicates that the node info is not available yet, + // and the profile doesn't need to be saved on disk. + if lp.ID != "" { + pm.knownProfiles[lp.ID] = lp + if err := pm.writeKnownProfiles(); err != nil { + return err + } + // Clone prefsIn and create a read-only view as a safety measure to + // prevent accidental preference mutations, both externally and internally. + if err := pm.setProfilePrefsNoPermCheck(lp, prefsIn.AsStruct().View()); err != nil { + return err + } } return nil } @@ -278,19 +319,35 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile } } -// setPrefsLocked sets the current profile's prefs to the provided value. -// It also saves the prefs to the StateStore, if the current profile -// is not new. -func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error { - pm.prefs = clonedPrefs - pm.updateHealth() - if pm.currentProfile.ID == "" { - return nil +// setProfilePrefsNoPermCheck sets the profile's prefs to the provided value. +// If the profile has the [ipn.LoginProfile.Key] set, it saves the prefs to the +// [ipn.StateStore] under that key. It returns an error if the profile is non-current +// and does not have its Key set, or if the prefs could not be saved. +// The method does not perform any additional checks on the specified +// profile, such as verifying the caller's access rights or checking +// if another profile for the same node already exists. +func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error { + isCurrentProfile := pm.currentProfile == profile + if isCurrentProfile { + pm.prefs = clonedPrefs + pm.updateHealth() } - if err := pm.writePrefsToStore(pm.currentProfile.Key, pm.prefs); err != nil { - return err + if profile.Key != "" { + if err := pm.writePrefsToStore(profile.Key, clonedPrefs); err != nil { + return err + } + } else if !isCurrentProfile { + return errors.New("cannot set prefs for a non-current in-memory profile") } - return pm.setUnattendedModeAsConfigured() + if isCurrentProfile { + return pm.setUnattendedModeAsConfigured() + } + return nil +} + +// setPrefsNoPermCheck is like [profileManager.setProfilePrefsNoPermCheck], but sets the current profile's prefs. +func (pm *profileManager) setPrefsNoPermCheck(clonedPrefs ipn.PrefsView) error { + return pm.setProfilePrefsNoPermCheck(pm.currentProfile, clonedPrefs) } func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsView) error { @@ -304,18 +361,67 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie return nil } -// Profiles returns the list of known profiles. +// Profiles returns the list of known profiles accessible to the current user. func (pm *profileManager) Profiles() []ipn.LoginProfile { allProfiles := pm.allProfiles() - out := make([]ipn.LoginProfile, 0, len(allProfiles)) - for _, p := range allProfiles { - out = append(out, *p) + out := make([]ipn.LoginProfile, len(allProfiles)) + for i, p := range allProfiles { + out[i] = *p } return out } +// ProfileByID returns a profile with the given id, if it is accessible to the current user. +// 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) ProfileByID(id ipn.ProfileID) (ipn.LoginProfile, error) { + kp, err := pm.profileByIDNoPermCheck(id) + if err != nil { + return ipn.LoginProfile{}, err + } + if err := pm.checkProfileAccess(kp); err != nil { + return ipn.LoginProfile{}, err + } + return *kp, nil +} + +// profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't +// check user's access rights to the profile. +func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (*ipn.LoginProfile, error) { + if id == pm.currentProfile.ID { + return pm.currentProfile, nil + } + kp, ok := pm.knownProfiles[id] + if !ok { + return nil, errProfileNotFound + } + return kp, nil +} + +// ProfilePrefs returns preferences for a profile with the given id. +// 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) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error) { + kp, err := pm.profileByIDNoPermCheck(id) + if err != nil { + return ipn.PrefsView{}, errProfileNotFound + } + if err := pm.checkProfileAccess(kp); err != nil { + return ipn.PrefsView{}, err + } + return pm.profilePrefs(kp) +} + +func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, error) { + if p.ID == pm.currentProfile.ID { + return pm.prefs, nil + } + return pm.loadSavedPrefs(p.Key) +} + // SwitchProfile switches to the profile with the given id. -// If the profile is not known, it returns an errProfileNotFound. +// 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) @@ -323,12 +429,12 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error { if !ok { return errProfileNotFound } - if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() { return nil } - if kp.LocalUserID != pm.currentUserID { - return fmt.Errorf("profile %q is not owned by current user", id) + + 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 { @@ -337,12 +443,32 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error { pm.prefs = prefs pm.updateHealth() pm.currentProfile = kp - return pm.setAsUserSelectedProfileLocked() + return pm.setProfileAsUserDefault(kp) } -func (pm *profileManager) setAsUserSelectedProfileLocked() error { +// 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, +// 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 +} + +// setProfileAsUserDefault sets the specified profile as the default for the current user. +// It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user. +func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) error { + if profile.Key == "" { + // The profile has not been persisted yet; ignore it for now. + return nil + } + if err := pm.checkProfileAccess(profile); err != nil { + return errProfileAccessDenied + } k := ipn.CurrentProfileKey(string(pm.currentUserID)) - return pm.WriteState(k, []byte(pm.currentProfile.Key)) + return pm.WriteState(k, []byte(profile.Key)) } func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) { @@ -387,53 +513,94 @@ func (pm *profileManager) CurrentProfile() ipn.LoginProfile { return *pm.currentProfile } -// errProfileNotFound is returned by methods that accept a ProfileID. +// errProfileNotFound is returned by methods that accept a ProfileID +// when the specified profile does not exist. var errProfileNotFound = errors.New("profile not found") +// errProfileAccessDenied is returned by methods that accept a ProfileID +// when the current user does not have access to the specified profile. +// It is used temporarily until we implement access checks based on the +// caller's identity in tailscale/corp#18342. +var errProfileAccessDenied = errors.New("profile access denied") + // DeleteProfile removes the profile with the given id. It returns -// errProfileNotFound if the profile does not exist. +// [errProfileNotFound] if the profile does not exist, or an +// [errProfileAccessDenied] if the specified profile is not accessible +// to the current user. // If the profile is the current profile, it is the equivalent of -// calling NewProfile() followed by DeleteProfile(id). This is -// useful for deleting the last profile. In other cases, it is -// recommended to call SwitchProfile() first. +// calling [profileManager.NewProfile] followed by [profileManager.DeleteProfile](id). +// This is useful for deleting the last profile. In other cases, it is +// recommended to call [profileManager.SwitchProfile] first. func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error { metricDeleteProfile.Add(1) - - if id == "" { - // Deleting the in-memory only new profile, just create a new one. - pm.NewProfile() - return nil + if id == pm.currentProfile.ID { + return pm.deleteCurrentProfile() } kp, ok := pm.knownProfiles[id] if !ok { return errProfileNotFound } - if kp.ID == pm.currentProfile.ID { - pm.NewProfile() - } - if err := pm.WriteState(kp.Key, nil); err != nil { + if err := pm.checkProfileAccess(kp); err != nil { return err } - delete(pm.knownProfiles, id) + return pm.deleteProfileNoPermCheck(kp) +} + +func (pm *profileManager) deleteCurrentProfile() error { + if err := pm.checkProfileAccess(pm.currentProfile); err != nil { + return err + } + if pm.currentProfile.ID == "" { + // Deleting the in-memory only new profile, just create a new one. + pm.NewProfile() + return nil + } + return pm.deleteProfileNoPermCheck(pm.currentProfile) +} + +// deleteProfileNoPermCheck is like [profileManager.DeleteProfile], +// but it doesn't check user's access rights to the profile. +func (pm *profileManager) deleteProfileNoPermCheck(profile *ipn.LoginProfile) error { + if profile.ID == pm.currentProfile.ID { + pm.NewProfile() + } + if err := pm.WriteState(profile.Key, nil); err != nil { + return err + } + delete(pm.knownProfiles, profile.ID) return pm.writeKnownProfiles() } -// DeleteAllProfiles removes all known profiles and switches to a new empty -// profile. -func (pm *profileManager) DeleteAllProfiles() error { +// DeleteAllProfilesForUser removes all known profiles accessible to the current user +// and switches to a new, empty profile. +func (pm *profileManager) DeleteAllProfilesForUser() error { metricDeleteAllProfile.Add(1) + currentProfileDeleted := false + writeKnownProfiles := func() error { + if currentProfileDeleted || pm.currentProfile.ID == "" { + pm.NewProfile() + } + return pm.writeKnownProfiles() + } + for _, kp := range pm.knownProfiles { + if pm.checkProfileAccess(kp) != nil { + // Skip profiles we don't have access to. + continue + } if err := pm.WriteState(kp.Key, nil); err != nil { // Write to remove references to profiles we've already deleted, but // return the original error. - pm.writeKnownProfiles() + writeKnownProfiles() return err } delete(pm.knownProfiles, kp.ID) + if kp.ID == pm.currentProfile.ID { + currentProfileDeleted = true + } } - pm.NewProfile() - return pm.writeKnownProfiles() + return writeKnownProfiles() } func (pm *profileManager) writeKnownProfiles() error { @@ -452,13 +619,43 @@ func (pm *profileManager) updateHealth() { } // NewProfile creates and switches to a new unnamed profile. The new profile is -// not persisted until SetPrefs is called with a logged-in user. +// not persisted until [profileManager.SetPrefs] is called with a logged-in user. func (pm *profileManager) NewProfile() { + pm.NewProfileForUser(pm.currentUserID) +} + +// NewProfileForUser is like [profileManager.NewProfile], 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) pm.prefs = defaultPrefs pm.updateHealth() - pm.currentProfile = &ipn.LoginProfile{} + pm.currentProfile = &ipn.LoginProfile{LocalUserID: uid} +} + +// newProfileWithPrefs creates a new profile with the specified prefs and assigns +// the specified uid as the profile owner. If switchNow is true, it switches to the +// newly created profile immediately. It returns the newly created profile on success, +// or an error on failure. +func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) { + metricNewProfile.Add(1) + + profile := &ipn.LoginProfile{LocalUserID: uid} + if err := pm.SetProfilePrefs(profile, prefs, ipn.NetworkProfile{}); err != nil { + return nil, err + } + if switchNow { + pm.currentProfile = profile + pm.prefs = prefs.AsStruct().View() + pm.updateHealth() + if err := pm.setProfileAsUserDefault(profile); err != nil { + return nil, err + } + } + return profile, nil } // defaultPrefs is the default prefs for a new profile. This initializes before @@ -473,7 +670,7 @@ var defaultPrefs = func() ipn.PrefsView { return prefs.View() }() -// Store returns the StateStore used by the ProfileManager. +// Store returns the [ipn.StateStore] used by the [profileManager]. func (pm *profileManager) Store() ipn.StateStore { return pm.store } @@ -494,8 +691,8 @@ func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsV return pm.CurrentPrefs(), nil } -// newProfileManager creates a new ProfileManager using the provided StateStore. -// It also loads the list of known profiles from the StateStore. +// newProfileManager creates a new [profileManager] using the provided [ipn.StateStore]. +// It also loads the list of known profiles from the store. func newProfileManager(store ipn.StateStore, logf logger.Logf, health *health.Tracker) (*profileManager, error) { return newProfileManagerWithGOOS(store, logf, health, envknob.GOOS()) } @@ -543,6 +740,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt } pm := &profileManager{ + goos: goos, store: store, knownProfiles: knownProfiles, logf: logf, @@ -567,7 +765,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt if err != nil { return nil, err } - if err := pm.setPrefsLocked(prefs); err != nil { + if err := pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefs); err != nil { return nil, err } // Most platform behavior is controlled by the goos parameter, however @@ -580,7 +778,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt } else if len(knownProfiles) == 0 && goos != "windows" && runtime.GOOS != "windows" { // No known profiles, try a migration. pm.dlogf("no known profiles; trying to migrate from legacy prefs") - if err := pm.migrateFromLegacyPrefs(); err != nil { + if _, err := pm.migrateFromLegacyPrefs(pm.currentUserID, true); err != nil { return nil, err } } else { @@ -590,23 +788,23 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt return pm, nil } -func (pm *profileManager) migrateFromLegacyPrefs() error { +func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (*ipn.LoginProfile, error) { metricMigration.Add(1) - pm.NewProfile() - sentinel, prefs, err := pm.loadLegacyPrefs() + sentinel, prefs, err := pm.loadLegacyPrefs(uid) if err != nil { metricMigrationError.Add(1) - return fmt.Errorf("load legacy prefs: %w", err) + return nil, fmt.Errorf("load legacy prefs: %w", err) } pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel) - if err := pm.SetPrefs(prefs, ipn.NetworkProfile{}); err != nil { + profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow) + if err != nil { metricMigrationError.Add(1) - return fmt.Errorf("migrating _daemon profile: %w", err) + return nil, fmt.Errorf("migrating _daemon profile: %w", err) } pm.completeMigration(sentinel) pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel) metricMigrationSuccess.Add(1) - return nil + return profile, nil } func (pm *profileManager) requiresBackfill() bool { diff --git a/ipn/ipnlocal/profiles_notwindows.go b/ipn/ipnlocal/profiles_notwindows.go index fc61d2671..0ca8f439c 100644 --- a/ipn/ipnlocal/profiles_notwindows.go +++ b/ipn/ipnlocal/profiles_notwindows.go @@ -13,7 +13,7 @@ import ( "tailscale.com/version" ) -func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) { +func (pm *profileManager) loadLegacyPrefs(ipn.WindowsUserID) (string, ipn.PrefsView, error) { k := ipn.LegacyGlobalDaemonStateKey switch { case runtime.GOOS == "ios", version.IsSandboxedMacOS(): diff --git a/ipn/ipnlocal/profiles_test.go b/ipn/ipnlocal/profiles_test.go index 01d49c230..73e4f6535 100644 --- a/ipn/ipnlocal/profiles_test.go +++ b/ipn/ipnlocal/profiles_test.go @@ -540,9 +540,7 @@ func TestProfileManagementWindows(t *testing.T) { { t.Logf("Set user1 as logged in user") - if err := pm.SetCurrentUserID(uid); err != nil { - t.Fatalf("can't set user id: %s", err) - } + pm.SetCurrentUserID(uid) checkProfiles(t) t.Logf("Save prefs for user1") wantProfiles["default"] = setPrefs(t, "default", false) @@ -576,9 +574,7 @@ func TestProfileManagementWindows(t *testing.T) { { t.Logf("Set user1 as current user") - if err := pm.SetCurrentUserID(uid); err != nil { - t.Fatal(err) - } + pm.SetCurrentUserID(uid) wantCurProfile = "test" } checkProfiles(t) diff --git a/ipn/ipnlocal/profiles_windows.go b/ipn/ipnlocal/profiles_windows.go index d98f4b526..c4beb22f9 100644 --- a/ipn/ipnlocal/profiles_windows.go +++ b/ipn/ipnlocal/profiles_windows.go @@ -22,6 +22,8 @@ const ( legacyPrefsExt = ".conf" ) +var errAlreadyMigrated = errors.New("profile migration already completed") + func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) { // TODO(aaron): Ideally we'd have the impersonation token for the pipe's // client and use it to call SHGetKnownFolderPath, thus yielding the correct @@ -37,10 +39,10 @@ func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) { return userLegacyPrefsDir, nil } -func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) { - userLegacyPrefsDir, err := legacyPrefsDir(pm.currentUserID) +func (pm *profileManager) loadLegacyPrefs(uid ipn.WindowsUserID) (string, ipn.PrefsView, error) { + userLegacyPrefsDir, err := legacyPrefsDir(uid) if err != nil { - pm.dlogf("no legacy preferences directory for %q: %v", pm.currentUserID, err) + pm.dlogf("no legacy preferences directory for %q: %v", uid, err) return "", ipn.PrefsView{}, err }