@ -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 a n [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 ( " SetCurrentUser ID: ReadState(%q) = %v, %v", string ( uid ) , len ( b ) , err )
pm . dlogf ( " DefaultUserProfile ID: 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 p m. 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
}
}
// findMatchin Profiles returns all profiles that represent the same node/user as
// findMatchin gProfiles 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 . Prefs View ) [ ] * 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 ( "[un xpected] multiple profiles with the same name")
pm . logf ( "[un e xpected] 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 ( "[un xpected] multiple profiles with the same key")
pm . logf ( "[un e xpected] 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 . set AsUserSelectedProfileLocked( )
return pm . set ProfileAsUserDefault( 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 ( p rofile. 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
// DeleteAllProfiles ForUser 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 ) DeleteAllProfiles ForUser ( ) 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 StateS tore.
// It also loads the list of known profiles from the s tore.
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 . setPr efsLocked( prefs ) ; err != nil {
if err := pm . setPr ofilePrefsNoPermCheck( 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 {