ipn/ipnlocal: refactor and cleanup profileManager

In preparation for multi-user and unattended mode improvements, we are
refactoring and cleaning up `ipn/ipnlocal.profileManager`. The concept of the
"current user", which is only relevant on Windows, is being deprecated and will
soon be removed to allow more than one Windows user to connect and utilize
`LocalBackend` according to that user's access rights to the device and specific
Tailscale profiles.

We plan to pass the user's identity down to the `profileManager`, where it can
be used to determine the user's access rights to a given `LoginProfile`. While
the new permission model in `ipnauth` requires more work and is currently
blocked pending PR reviews, we are updating the `profileManager` to reduce its
reliance on the concept of a single OS user being connected to the backend at
the same time.

We extract the switching to the default Tailscale profile, which may also
trigger legacy profile migration, from `profileManager.SetCurrentUserID`. This
introduces `profileManager.DefaultUserProfileID`, which returns the default
profile ID for the current user, and `profileManager.SwitchToDefaultProfile`,
which is essentially a shorthand for `pm.SwitchProfile(pm.DefaultUserProfileID())`.
Both methods will eventually be updated to accept the user's identity and
utilize that user's default profile.

We make access checks more explicit by introducing the `profileManager.checkProfileAccess`
method. The current implementation continues to use `profileManager.currentUserID`
and `LoginProfile.LocalUserID` to determine whether access to a given profile
should be granted. This will be updated to utilize the `ipnauth` package and the
new permissions model once it's ready. We also expand access checks to be used
more widely in the `profileManager`, not just when switching or listing
profiles. This includes access checks in methods like `SetPrefs` and, most notably,
`DeleteProfile` and `DeleteAllProfiles`, preventing unprivileged Windows users
from deleting Tailscale profiles owned by other users on the same device,
including profiles owned by local admins.

We extract `profileManager.ProfilePrefs` and `profileManager.SetProfilePrefs`
methods that can be used to get and set preferences of a given `LoginProfile` if
`profileManager.checkProfileAccess` permits access to it.

We also update `profileManager.setUnattendedModeAsConfigured` to always enable
unattended mode on Windows if `Prefs.ForceDaemon` is true in the current
`LoginProfile`, even if `profileManager.currentUserID` is `""`. This facilitates
enabling unattended mode via `tailscale up --unattended` even if
`tailscale-ipn.exe` is not running, such as when a Group Policy or MDM-deployed
script runs at boot time, or when Tailscale is used on a Server Code or otherwise
headless Windows environments. See #12239, #2137, #3186 and
https://github.com/tailscale/tailscale/pull/6255#issuecomment-2016623838 for
details.

Fixes #12239
Updates tailscale/corp#18342
Updates #3186
Updates #2137

Signed-off-by: Nick Khyl <nickk@tailscale.com>
pull/13302/head
Nick Khyl 3 months ago committed by Nick Khyl
parent 73b3c8fc8c
commit 80b2b45d60

@ -243,7 +243,7 @@ func (b *LocalBackend) driveSetSharesLocked(shares []*drive.Share) error {
}, },
DriveSharesSet: true, DriveSharesSet: true,
}) })
return b.pm.setPrefsLocked(prefs.View()) return b.pm.setPrefsNoPermCheck(prefs.View())
} }
// driveNotifyShares notifies IPN bus listeners (e.g. Mac Application process) // driveNotifyShares notifies IPN bus listeners (e.g. Mac Application process)

@ -3321,9 +3321,7 @@ func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) (ipn.WindowsUserID, e
if b.pm.CurrentUserID() == uid { if b.pm.CurrentUserID() == uid {
return uid, nil return uid, nil
} }
if err := b.pm.SetCurrentUserID(uid); err != nil { b.pm.SetCurrentUserID(uid)
return uid, nil
}
if c, ok := b.currentUser.(ipnauth.ActorCloser); ok { if c, ok := b.currentUser.(ipnauth.ActorCloser); ok {
c.Close() c.Close()
} }
@ -6575,7 +6573,7 @@ func (b *LocalBackend) ResetAuth() error {
if err := b.clearMachineKeyLocked(); err != nil { if err := b.clearMachineKeyLocked(); err != nil {
return err return err
} }
if err := b.pm.DeleteAllProfiles(); err != nil { if err := b.pm.DeleteAllProfilesForUser(); err != nil {
return err return err
} }
b.resetDialPlan() // always reset if we're removing everything b.resetDialPlan() // always reset if we're removing everything

@ -2654,7 +2654,7 @@ func TestOnTailnetDefaultAutoUpdate(t *testing.T) {
b.hostinfo.Container = tt.container b.hostinfo.Container = tt.container
p := ipn.NewPrefs() p := ipn.NewPrefs()
p.AutoUpdate.Apply = tt.before 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) t.Fatal(err)
} }
b.onTailnetDefaultAutoUpdate(tt.tailnetDefault) b.onTailnetDefaultAutoUpdate(tt.tailnetDefault)

@ -17,19 +17,19 @@ import (
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health" "tailscale.com/health"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
) )
var errAlreadyMigrated = errors.New("profile migration already completed")
var debug = envknob.RegisterBool("TS_DEBUG_PROFILES") 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. // multiple profiles and the current profile.
// //
// It is not safe for concurrent use. // It is not safe for concurrent use.
type profileManager struct { type profileManager struct {
goos string // used for TestProfileManagementWindows
store ipn.StateStore store ipn.StateStore
logf logger.Logf logf logger.Logf
health *health.Tracker health *health.Tracker
@ -57,61 +57,68 @@ func (pm *profileManager) CurrentUserID() ipn.WindowsUserID {
return pm.currentUserID return pm.currentUserID
} }
// SetCurrentUserID sets the current user ID. The uid is only non-empty // SetCurrentUserID sets the current user ID and switches to that user's default (last used) profile.
// on Windows where we have a multi-user system. // If the specified user does not have a default profile, or the default profile could not be loaded,
func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error { // 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 { if pm.currentUserID == uid {
return nil return
} }
prev := pm.currentUserID
pm.currentUserID = uid pm.currentUserID = uid
if uid == "" && prev != "" { if err := pm.SwitchToDefaultProfile(); err != nil {
// This is a local user logout, or app shutdown. // SetCurrentUserID should never fail and must always switch to the
// Clear the current profile. // user's default profile or create a new profile for the current user.
pm.NewProfile() // Until we implement multi-user support and the new permission model,
return nil // 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 // 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))) 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 err == ipn.ErrStateNotExist || len(b) == 0 {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
pm.dlogf("SetCurrentUserID: windows: migrating from legacy preferences") pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences")
if err := pm.migrateFromLegacyPrefs(); err != nil && !errors.Is(err, errAlreadyMigrated) { profile, err := pm.migrateFromLegacyPrefs(uid, false)
return err if err == nil {
return profile.ID
} }
} else { pm.logf("failed to migrate from legacy preferences: %v", err)
pm.NewProfile()
} }
return nil return ""
} }
// Now attempt to load the profile using the key we just read.
pk := ipn.StateKey(string(b)) pk := ipn.StateKey(string(b))
prof := pm.findProfileByKey(pk) prof := pm.findProfileByKey(pk)
if prof == nil { if prof == nil {
pm.dlogf("SetCurrentUserID: no profile found for key: %q", pk) pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
pm.NewProfile() return ""
return nil
} }
prefs, err := pm.loadSavedPrefs(pk) return prof.ID
if err != nil { }
pm.NewProfile()
return err // 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 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. // The returned profiles are sorted by Name.
func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) { func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
for _, p := range pm.knownProfiles { for _, p := range pm.knownProfiles {
if p.LocalUserID == pm.currentUserID { if pm.checkProfileAccess(p) == nil {
out = append(out, p) out = append(out, p)
} }
} }
@ -121,9 +128,8 @@ func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
return out return out
} }
// matchingProfiles returns all profiles that match the given predicate and // matchingProfiles is like [profileManager.allProfiles], but returns only profiles
// belong to the currentUserID. // matching the given predicate.
// The returned profiles are sorted by Name.
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) { func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
all := pm.allProfiles() all := pm.allProfiles()
out = all[:0] out = all[:0]
@ -135,19 +141,20 @@ func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out
return out return out
} }
// findMatchinProfiles returns all profiles that represent the same node/user as // findMatchingProfiles returns all profiles accessible to the current user
// prefs. // that represent the same node/user as prefs.
// The returned profiles are sorted by Name. // 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 pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
return p.ControlURL == prefs.ControlURL && return p.ControlURL == prefs.ControlURL() &&
(p.UserProfile.ID == prefs.Persist.UserProfile.ID || (p.UserProfile.ID == prefs.Persist().UserProfile().ID ||
p.NodeID == prefs.Persist.NodeID) p.NodeID == prefs.Persist().NodeID())
}) })
} }
// ProfileIDForName returns the profile ID for the profile with the // 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 { func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
p := pm.findProfileByName(name) p := pm.findProfileByName(name)
if p == nil { if p == nil {
@ -164,7 +171,7 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
return nil return nil
} }
if len(out) > 1 { 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] return out[0]
} }
@ -177,17 +184,17 @@ func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
return nil return nil
} }
if len(out) > 1 { 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] return out[0]
} }
func (pm *profileManager) setUnattendedModeAsConfigured() error { func (pm *profileManager) setUnattendedModeAsConfigured() error {
if pm.currentUserID == "" { if pm.goos != "windows" {
return nil return nil
} }
if pm.prefs.ForceDaemon() { if pm.currentProfile.Key != "" && pm.prefs.ForceDaemon() {
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key)) return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
} else { } else {
return pm.WriteState(ipn.ServerModeStartKey, nil) 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. // 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 // It also saves the prefs to the [ipn.StateStore]. It stores a copy of the
// provided prefs, which may be accessed via CurrentPrefs. // 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 // 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. // across user switches to disambiguate the same account but a different tailnet.
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error { func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
prefs := prefsIn.AsStruct() cp := pm.currentProfile
newPersist := prefs.Persist if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName == "" {
if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" {
// We don't know anything about this profile, so ignore it for now. // We don't know anything about this profile, so ignore it for now.
return pm.setPrefsLocked(prefs.View()) return pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefsIn.AsStruct().View())
}
up := newPersist.UserProfile
if up.DisplayName == "" {
up.DisplayName = up.LoginName
} }
cp := pm.currentProfile
// Check if we already have an existing profile that matches the user/node. // 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 // We already have a profile for this user/node we should reuse it. Also
// cleanup any other duplicate profiles. // cleanup any other duplicate profiles.
cp = existing[0] 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. // We couldn't delete the state, so keep the profile around.
continue 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) 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.
}
if prefs.ProfileName != "" {
cp.Name = prefs.ProfileName
} else {
cp.Name = up.LoginName
} }
cp.ControlURL = prefs.ControlURL
cp.UserProfile = newPersist.UserProfile
cp.NodeID = newPersist.NodeID
cp.NetworkProfile = np
pm.knownProfiles[cp.ID] = cp
pm.currentProfile = cp pm.currentProfile = cp
if err := pm.writeKnownProfiles(); err != nil { if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil {
return err return err
} }
if err := pm.setAsUserSelectedProfileLocked(); err != nil { 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 return err
} }
if err := pm.setPrefsLocked(prefs.View()); err != nil {
return err // 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 {
lp.NodeID = ""
}
if prefsIn.ProfileName() != "" {
lp.Name = prefsIn.ProfileName()
} else {
lp.Name = up.LoginName
}
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 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. // setProfilePrefsNoPermCheck sets the profile's prefs to the provided value.
// It also saves the prefs to the StateStore, if the current profile // If the profile has the [ipn.LoginProfile.Key] set, it saves the prefs to the
// is not new. // [ipn.StateStore] under that key. It returns an error if the profile is non-current
func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error { // and does not have its Key set, or if the prefs could not be saved.
pm.prefs = clonedPrefs // The method does not perform any additional checks on the specified
pm.updateHealth() // profile, such as verifying the caller's access rights or checking
if pm.currentProfile.ID == "" { // if another profile for the same node already exists.
return nil 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 { if profile.Key != "" {
return err 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 { 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 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 { func (pm *profileManager) Profiles() []ipn.LoginProfile {
allProfiles := pm.allProfiles() allProfiles := pm.allProfiles()
out := make([]ipn.LoginProfile, 0, len(allProfiles)) out := make([]ipn.LoginProfile, len(allProfiles))
for _, p := range allProfiles { for i, p := range allProfiles {
out = append(out, *p) out[i] = *p
} }
return out 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. // 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 { func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
metricSwitchProfile.Add(1) metricSwitchProfile.Add(1)
@ -323,12 +429,12 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
if !ok { if !ok {
return errProfileNotFound return errProfileNotFound
} }
if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() { if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() {
return nil 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) prefs, err := pm.loadSavedPrefs(kp.Key)
if err != nil { if err != nil {
@ -337,12 +443,32 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
pm.prefs = prefs pm.prefs = prefs
pm.updateHealth() pm.updateHealth()
pm.currentProfile = kp 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)) 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) { func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
@ -387,53 +513,94 @@ func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
return *pm.currentProfile 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") 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 // 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 // If the profile is the current profile, it is the equivalent of
// calling NewProfile() followed by DeleteProfile(id). This is // calling [profileManager.NewProfile] followed by [profileManager.DeleteProfile](id).
// useful for deleting the last profile. In other cases, it is // This is useful for deleting the last profile. In other cases, it is
// recommended to call SwitchProfile() first. // recommended to call [profileManager.SwitchProfile] first.
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error { func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
metricDeleteProfile.Add(1) metricDeleteProfile.Add(1)
if id == pm.currentProfile.ID {
if id == "" { return pm.deleteCurrentProfile()
// Deleting the in-memory only new profile, just create a new one.
pm.NewProfile()
return nil
} }
kp, ok := pm.knownProfiles[id] kp, ok := pm.knownProfiles[id]
if !ok { if !ok {
return errProfileNotFound return errProfileNotFound
} }
if kp.ID == pm.currentProfile.ID { if err := pm.checkProfileAccess(kp); err != nil {
return err
}
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() pm.NewProfile()
return nil
} }
if err := pm.WriteState(kp.Key, nil); err != 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 return err
} }
delete(pm.knownProfiles, id) delete(pm.knownProfiles, profile.ID)
return pm.writeKnownProfiles() return pm.writeKnownProfiles()
} }
// DeleteAllProfiles removes all known profiles and switches to a new empty // DeleteAllProfilesForUser removes all known profiles accessible to the current user
// profile. // and switches to a new, empty profile.
func (pm *profileManager) DeleteAllProfiles() error { func (pm *profileManager) DeleteAllProfilesForUser() error {
metricDeleteAllProfile.Add(1) metricDeleteAllProfile.Add(1)
currentProfileDeleted := false
writeKnownProfiles := func() error {
if currentProfileDeleted || pm.currentProfile.ID == "" {
pm.NewProfile()
}
return pm.writeKnownProfiles()
}
for _, kp := range pm.knownProfiles { 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 { if err := pm.WriteState(kp.Key, nil); err != nil {
// Write to remove references to profiles we've already deleted, but // Write to remove references to profiles we've already deleted, but
// return the original error. // return the original error.
pm.writeKnownProfiles() writeKnownProfiles()
return err return err
} }
delete(pm.knownProfiles, kp.ID) delete(pm.knownProfiles, kp.ID)
if kp.ID == pm.currentProfile.ID {
currentProfileDeleted = true
}
} }
pm.NewProfile() return writeKnownProfiles()
return pm.writeKnownProfiles()
} }
func (pm *profileManager) writeKnownProfiles() error { 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 // 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() { 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) metricNewProfile.Add(1)
pm.prefs = defaultPrefs pm.prefs = defaultPrefs
pm.updateHealth() 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 // defaultPrefs is the default prefs for a new profile. This initializes before
@ -473,7 +670,7 @@ var defaultPrefs = func() ipn.PrefsView {
return prefs.View() 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 { func (pm *profileManager) Store() ipn.StateStore {
return pm.store return pm.store
} }
@ -494,8 +691,8 @@ func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsV
return pm.CurrentPrefs(), nil return pm.CurrentPrefs(), nil
} }
// newProfileManager creates a new ProfileManager using the provided StateStore. // newProfileManager creates a new [profileManager] using the provided [ipn.StateStore].
// It also loads the list of known profiles from the 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) { func newProfileManager(store ipn.StateStore, logf logger.Logf, health *health.Tracker) (*profileManager, error) {
return newProfileManagerWithGOOS(store, logf, health, envknob.GOOS()) return newProfileManagerWithGOOS(store, logf, health, envknob.GOOS())
} }
@ -543,6 +740,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
} }
pm := &profileManager{ pm := &profileManager{
goos: goos,
store: store, store: store,
knownProfiles: knownProfiles, knownProfiles: knownProfiles,
logf: logf, logf: logf,
@ -567,7 +765,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := pm.setPrefsLocked(prefs); err != nil { if err := pm.setProfilePrefsNoPermCheck(pm.currentProfile, prefs); err != nil {
return nil, err return nil, err
} }
// Most platform behavior is controlled by the goos parameter, however // 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" { } else if len(knownProfiles) == 0 && goos != "windows" && runtime.GOOS != "windows" {
// No known profiles, try a migration. // No known profiles, try a migration.
pm.dlogf("no known profiles; trying to migrate from legacy prefs") 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 return nil, err
} }
} else { } else {
@ -590,23 +788,23 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
return pm, nil return pm, nil
} }
func (pm *profileManager) migrateFromLegacyPrefs() error { func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (*ipn.LoginProfile, error) {
metricMigration.Add(1) metricMigration.Add(1)
pm.NewProfile() sentinel, prefs, err := pm.loadLegacyPrefs(uid)
sentinel, prefs, err := pm.loadLegacyPrefs()
if err != nil { if err != nil {
metricMigrationError.Add(1) 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) 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) metricMigrationError.Add(1)
return fmt.Errorf("migrating _daemon profile: %w", err) return nil, fmt.Errorf("migrating _daemon profile: %w", err)
} }
pm.completeMigration(sentinel) pm.completeMigration(sentinel)
pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel) pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
metricMigrationSuccess.Add(1) metricMigrationSuccess.Add(1)
return nil return profile, nil
} }
func (pm *profileManager) requiresBackfill() bool { func (pm *profileManager) requiresBackfill() bool {

@ -13,7 +13,7 @@ import (
"tailscale.com/version" "tailscale.com/version"
) )
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) { func (pm *profileManager) loadLegacyPrefs(ipn.WindowsUserID) (string, ipn.PrefsView, error) {
k := ipn.LegacyGlobalDaemonStateKey k := ipn.LegacyGlobalDaemonStateKey
switch { switch {
case runtime.GOOS == "ios", version.IsSandboxedMacOS(): case runtime.GOOS == "ios", version.IsSandboxedMacOS():

@ -540,9 +540,7 @@ func TestProfileManagementWindows(t *testing.T) {
{ {
t.Logf("Set user1 as logged in user") t.Logf("Set user1 as logged in user")
if err := pm.SetCurrentUserID(uid); err != nil { pm.SetCurrentUserID(uid)
t.Fatalf("can't set user id: %s", err)
}
checkProfiles(t) checkProfiles(t)
t.Logf("Save prefs for user1") t.Logf("Save prefs for user1")
wantProfiles["default"] = setPrefs(t, "default", false) wantProfiles["default"] = setPrefs(t, "default", false)
@ -576,9 +574,7 @@ func TestProfileManagementWindows(t *testing.T) {
{ {
t.Logf("Set user1 as current user") t.Logf("Set user1 as current user")
if err := pm.SetCurrentUserID(uid); err != nil { pm.SetCurrentUserID(uid)
t.Fatal(err)
}
wantCurProfile = "test" wantCurProfile = "test"
} }
checkProfiles(t) checkProfiles(t)

@ -22,6 +22,8 @@ const (
legacyPrefsExt = ".conf" legacyPrefsExt = ".conf"
) )
var errAlreadyMigrated = errors.New("profile migration already completed")
func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) { func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) {
// TODO(aaron): Ideally we'd have the impersonation token for the pipe's // TODO(aaron): Ideally we'd have the impersonation token for the pipe's
// client and use it to call SHGetKnownFolderPath, thus yielding the correct // 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 return userLegacyPrefsDir, nil
} }
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) { func (pm *profileManager) loadLegacyPrefs(uid ipn.WindowsUserID) (string, ipn.PrefsView, error) {
userLegacyPrefsDir, err := legacyPrefsDir(pm.currentUserID) userLegacyPrefsDir, err := legacyPrefsDir(uid)
if err != nil { 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 return "", ipn.PrefsView{}, err
} }

Loading…
Cancel
Save