ipn: generate LoginProfileView and use it instead of *LoginProfile where appropriate

Conventionally, we use views (e.g., ipn.PrefsView, tailcfg.NodeView, etc.) when
dealing with structs that shouldn't be mutated. However, ipn.LoginProfile has been
an exception so far, with a mix of passing and returning LoginProfile by reference
(allowing accidental mutations) and by value (which is wasteful, given its
current size of 192 bytes).

In this PR, we generate an ipn.LoginProfileView and use it instead of passing/returning
LoginProfiles by mutable reference or copying them when passing/returning by value.
Now, LoginProfiles can only be mutated by (*profileManager).setProfilePrefs.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2025-01-30 11:24:25 -06:00 committed by Nick Khyl
parent 7d5fe13d27
commit 4e7f4086b2
12 changed files with 254 additions and 151 deletions

View File

@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig //go:generate go run tailscale.com/cmd/viewer -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// Package ipn implements the interactions between the Tailscale cloud // Package ipn implements the interactions between the Tailscale cloud
// control plane and the local network stack. // control plane and the local network stack.

View File

@ -17,6 +17,29 @@ import (
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
) )
// Clone makes a deep copy of LoginProfile.
// The result aliases no memory with the original.
func (src *LoginProfile) Clone() *LoginProfile {
if src == nil {
return nil
}
dst := new(LoginProfile)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileCloneNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
}{})
// Clone makes a deep copy of Prefs. // Clone makes a deep copy of Prefs.
// The result aliases no memory with the original. // The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs { func (src *Prefs) Clone() *Prefs {

View File

@ -18,7 +18,73 @@ import (
"tailscale.com/types/views" "tailscale.com/types/views"
) )
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig //go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// View returns a read-only view of LoginProfile.
func (p *LoginProfile) View() LoginProfileView {
return LoginProfileView{ж: p}
}
// LoginProfileView provides a read-only view over LoginProfile.
//
// Its methods should only be called if `Valid()` returns true.
type LoginProfileView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *LoginProfile
}
// Valid reports whether v's underlying value is non-nil.
func (v LoginProfileView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LoginProfileView) AsStruct() *LoginProfile {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v LoginProfileView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *LoginProfileView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x LoginProfile
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v LoginProfileView) ID() ProfileID { return v.ж.ID }
func (v LoginProfileView) Name() string { return v.ж.Name }
func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile }
func (v LoginProfileView) Key() StateKey { return v.ж.Key }
func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID }
func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileViewNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
}{})
// View returns a read-only view of Prefs. // View returns a read-only view of Prefs.
func (p *Prefs) View() PrefsView { func (p *Prefs) View() PrefsView {

View File

@ -4045,7 +4045,7 @@ func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error {
// No profile with that name exists. That's fine. // No profile with that name exists. That's fine.
return nil return nil
} }
if id != b.pm.CurrentProfile().ID { if id != b.pm.CurrentProfile().ID() {
// Name is already in use by another profile. // Name is already in use by another profile.
return fmt.Errorf("profile name %q already in use", p.ProfileName) return fmt.Errorf("profile name %q already in use", p.ProfileName)
} }
@ -4127,7 +4127,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
} }
prefs := newp.View() prefs := newp.View()
np := b.pm.CurrentProfile().NetworkProfile np := b.pm.CurrentProfile().NetworkProfile()
if netMap != nil { if netMap != nil {
np = ipn.NetworkProfile{ np = ipn.NetworkProfile{
MagicDNSName: b.netMap.MagicDNSSuffix(), MagicDNSName: b.netMap.MagicDNSSuffix(),
@ -5663,7 +5663,7 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
unlock = b.lockAndGetUnlock() unlock = b.lockAndGetUnlock()
defer unlock() defer unlock()
if err := b.pm.DeleteProfile(profile.ID); err != nil { if err := b.pm.DeleteProfile(profile.ID()); err != nil {
b.logf("error deleting profile: %v", err) b.logf("error deleting profile: %v", err)
return err return err
} }
@ -6039,7 +6039,7 @@ func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
// the method to only run the reset-logic and not reload the store from memory to ensure // the method to only run the reset-logic and not reload the store from memory to ensure
// foreground sessions are not removed if they are not saved on disk. // foreground sessions are not removed if they are not saved on disk.
func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID == "" { if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID() == "" {
// We're not logged in, so we don't have a profile. // We're not logged in, so we don't have a profile.
// Don't try to load the serve config. // Don't try to load the serve config.
b.lastServeConfJSON = mem.B(nil) b.lastServeConfJSON = mem.B(nil)
@ -6047,7 +6047,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
return return
} }
confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID) confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID())
// TODO(maisem,bradfitz): prevent reading the config from disk // TODO(maisem,bradfitz): prevent reading the config from disk
// if the profile has not changed. // if the profile has not changed.
confj, err := b.store.ReadState(confKey) confj, err := b.store.ReadState(confKey)
@ -7000,7 +7000,7 @@ func (b *LocalBackend) ShouldInterceptVIPServiceTCPPort(ap netip.AddrPort) bool
// It will restart the backend on success. // It will restart the backend on success.
// If the profile is not known, it returns an errProfileNotFound. // If the profile is not known, it returns an errProfileNotFound.
func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error { func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
if b.CurrentProfile().ID == profile { if b.CurrentProfile().ID() == profile {
return nil return nil
} }
unlock := b.lockAndGetUnlock() unlock := b.lockAndGetUnlock()
@ -7023,12 +7023,12 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
func (b *LocalBackend) initTKALocked() error { func (b *LocalBackend) initTKALocked() error {
cp := b.pm.CurrentProfile() cp := b.pm.CurrentProfile()
if cp.ID == "" { if cp.ID() == "" {
b.tka = nil b.tka = nil
return nil return nil
} }
if b.tka != nil { if b.tka != nil {
if b.tka.profile == cp.ID { if b.tka.profile == cp.ID() {
// Already initialized. // Already initialized.
return nil return nil
} }
@ -7058,7 +7058,7 @@ func (b *LocalBackend) initTKALocked() error {
} }
b.tka = &tkaState{ b.tka = &tkaState{
profile: cp.ID, profile: cp.ID(),
authority: authority, authority: authority,
storage: storage, storage: storage,
} }
@ -7111,7 +7111,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
unlock := b.lockAndGetUnlock() unlock := b.lockAndGetUnlock()
defer unlock() defer unlock()
needToRestart := b.pm.CurrentProfile().ID == p needToRestart := b.pm.CurrentProfile().ID() == p
if err := b.pm.DeleteProfile(p); err != nil { if err := b.pm.DeleteProfile(p); err != nil {
if err == errProfileNotFound { if err == errProfileNotFound {
return nil return nil
@ -7126,7 +7126,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
// CurrentProfile returns the current LoginProfile. // CurrentProfile returns the current LoginProfile.
// The value may be zero if the profile is not persisted. // The value may be zero if the profile is not persisted.
func (b *LocalBackend) CurrentProfile() ipn.LoginProfile { func (b *LocalBackend) CurrentProfile() ipn.LoginProfileView {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
return b.pm.CurrentProfile() return b.pm.CurrentProfile()
@ -7147,7 +7147,7 @@ func (b *LocalBackend) NewProfile() error {
} }
// ListProfiles returns a list of all LoginProfiles. // ListProfiles returns a list of all LoginProfiles.
func (b *LocalBackend) ListProfiles() []ipn.LoginProfile { func (b *LocalBackend) ListProfiles() []ipn.LoginProfileView {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
return b.pm.Profiles() return b.pm.Profiles()
@ -7353,7 +7353,7 @@ func (b *LocalBackend) UnadvertiseRoute(toRemove ...netip.Prefix) error {
// namespace a key with the profile manager's current profile key, if any // namespace a key with the profile manager's current profile key, if any
func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.StateKey { func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.StateKey {
return pm.CurrentProfile().Key + "||" + key return pm.CurrentProfile().Key() + "||" + key
} }
const routeInfoStateStoreKey ipn.StateKey = "_routeInfo" const routeInfoStateStoreKey ipn.StateKey = "_routeInfo"
@ -7361,7 +7361,7 @@ const routeInfoStateStoreKey ipn.StateKey = "_routeInfo"
func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error { func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
if b.pm.CurrentProfile().ID == "" { if b.pm.CurrentProfile().ID() == "" {
return nil return nil
} }
key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey) key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey)
@ -7373,7 +7373,7 @@ func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error {
} }
func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) { func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) {
if b.pm.CurrentProfile().ID == "" { if b.pm.CurrentProfile().ID() == "" {
return &appc.RouteInfo{}, nil return &appc.RouteInfo{}, nil
} }
key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey) key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey)

View File

@ -4087,9 +4087,9 @@ func TestReadWriteRouteInfo(t *testing.T) {
b := newTestBackend(t) b := newTestBackend(t)
prof1 := ipn.LoginProfile{ID: "id1", Key: "key1"} prof1 := ipn.LoginProfile{ID: "id1", Key: "key1"}
prof2 := ipn.LoginProfile{ID: "id2", Key: "key2"} prof2 := ipn.LoginProfile{ID: "id2", Key: "key2"}
b.pm.knownProfiles["id1"] = &prof1 b.pm.knownProfiles["id1"] = prof1.View()
b.pm.knownProfiles["id2"] = &prof2 b.pm.knownProfiles["id2"] = prof2.View()
b.pm.currentProfile = &prof1 b.pm.currentProfile = prof1.View()
// set up routeInfo // set up routeInfo
ri1 := &appc.RouteInfo{} ri1 := &appc.RouteInfo{}

View File

@ -407,7 +407,7 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) chonkPathLocked() string { func (b *LocalBackend) chonkPathLocked() string {
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID)) return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID()))
} }
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the // tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
@ -455,7 +455,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
} }
b.tka = &tkaState{ b.tka = &tkaState{
profile: b.pm.CurrentProfile().ID, profile: b.pm.CurrentProfile().ID(),
authority: authority, authority: authority,
storage: chonk, storage: chonk,
} }

View File

@ -202,7 +202,7 @@ func TestTKADisablementFlow(t *testing.T) {
}).View(), ipn.NetworkProfile{})) }).View(), ipn.NetworkProfile{}))
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath) chonk, err := tka.ChonkDir(tkaPath)
if err != nil { if err != nil {
@ -410,7 +410,7 @@ func TestTKASync(t *testing.T) {
} }
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
// Setup the TKA authority on the node. // Setup the TKA authority on the node.
nodeStorage, err := tka.ChonkDir(tkaPath) nodeStorage, err := tka.ChonkDir(tkaPath)
@ -710,7 +710,7 @@ func TestTKADisable(t *testing.T) {
}).View(), ipn.NetworkProfile{})) }).View(), ipn.NetworkProfile{}))
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(tkaPath) chonk, err := tka.ChonkDir(tkaPath)
@ -770,7 +770,7 @@ func TestTKADisable(t *testing.T) {
ccAuto: cc, ccAuto: cc,
logf: t.Logf, logf: t.Logf,
tka: &tkaState{ tka: &tkaState{
profile: pm.CurrentProfile().ID, profile: pm.CurrentProfile().ID(),
authority: authority, authority: authority,
storage: chonk, storage: chonk,
}, },
@ -805,7 +805,7 @@ func TestTKASign(t *testing.T) {
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath) chonk, err := tka.ChonkDir(tkaPath)
if err != nil { if err != nil {
@ -890,7 +890,7 @@ func TestTKAForceDisable(t *testing.T) {
}).View(), ipn.NetworkProfile{})) }).View(), ipn.NetworkProfile{}))
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath) chonk, err := tka.ChonkDir(tkaPath)
if err != nil { if err != nil {
@ -989,7 +989,7 @@ func TestTKAAffectedSigs(t *testing.T) {
tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath) chonk, err := tka.ChonkDir(tkaPath)
if err != nil { if err != nil {
@ -1124,7 +1124,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1} compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1}
temp := t.TempDir() temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID)) tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID()))
os.Mkdir(tkaPath, 0755) os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath) chonk, err := tka.ChonkDir(tkaPath)
if err != nil { if err != nil {

View File

@ -35,8 +35,8 @@ type profileManager struct {
health *health.Tracker health *health.Tracker
currentUserID ipn.WindowsUserID currentUserID ipn.WindowsUserID
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
currentProfile *ipn.LoginProfile // always non-nil currentProfile ipn.LoginProfileView // always Valid.
prefs ipn.PrefsView // always Valid. prefs ipn.PrefsView // always Valid.
} }
@ -89,7 +89,7 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences") pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences")
profile, err := pm.migrateFromLegacyPrefs(uid, false) profile, err := pm.migrateFromLegacyPrefs(uid, false)
if err == nil { if err == nil {
return profile.ID return profile.ID()
} }
pm.logf("failed to migrate from legacy preferences: %v", err) pm.logf("failed to migrate from legacy preferences: %v", err)
} }
@ -98,17 +98,17 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
pk := ipn.StateKey(string(b)) pk := ipn.StateKey(string(b))
prof := pm.findProfileByKey(pk) prof := pm.findProfileByKey(pk)
if prof == nil { if !prof.Valid() {
pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk) pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
return "" return ""
} }
return prof.ID return prof.ID()
} }
// checkProfileAccess returns an [errProfileAccessDenied] if the current user // checkProfileAccess returns an [errProfileAccessDenied] if the current user
// does not have access to the specified profile. // does not have access to the specified profile.
func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error { func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error {
if pm.currentUserID != "" && profile.LocalUserID != pm.currentUserID { if pm.currentUserID != "" && profile.LocalUserID() != pm.currentUserID {
return errProfileAccessDenied return errProfileAccessDenied
} }
return nil return nil
@ -116,21 +116,21 @@ func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error {
// allProfiles returns all profiles accessible to the current user. // 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.LoginProfileView) {
for _, p := range pm.knownProfiles { for _, p := range pm.knownProfiles {
if pm.checkProfileAccess(p) == nil { if pm.checkProfileAccess(p) == nil {
out = append(out, p) out = append(out, p)
} }
} }
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int { slices.SortFunc(out, func(a, b ipn.LoginProfileView) int {
return cmp.Compare(a.Name, b.Name) return cmp.Compare(a.Name(), b.Name())
}) })
return out return out
} }
// matchingProfiles is like [profileManager.allProfiles], but returns only profiles // matchingProfiles is like [profileManager.allProfiles], but returns only profiles
// matching the given predicate. // matching the given predicate.
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) { func (pm *profileManager) matchingProfiles(f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
all := pm.allProfiles() all := pm.allProfiles()
out = all[:0] out = all[:0]
for _, p := range all { for _, p := range all {
@ -144,11 +144,11 @@ func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out
// findMatchingProfiles returns all profiles accessible to the current user // findMatchingProfiles returns all profiles accessible to the current user
// that represent the same node/user as 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.PrefsView) []*ipn.LoginProfile { func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []ipn.LoginProfileView {
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool { return pm.matchingProfiles(func(p ipn.LoginProfileView) 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())
}) })
} }
@ -157,18 +157,18 @@ func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.Login
// accessible to the current user. // 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.Valid() {
return "" return ""
} }
return p.ID return p.ID()
} }
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile { func (pm *profileManager) findProfileByName(name string) ipn.LoginProfileView {
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool { out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
return p.Name == name return p.Name() == name
}) })
if len(out) == 0 { if len(out) == 0 {
return nil return ipn.LoginProfileView{}
} }
if len(out) > 1 { if len(out) > 1 {
pm.logf("[unexpected] multiple profiles with the same name") pm.logf("[unexpected] multiple profiles with the same name")
@ -176,12 +176,12 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
return out[0] return out[0]
} }
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile { func (pm *profileManager) findProfileByKey(key ipn.StateKey) ipn.LoginProfileView {
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool { out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
return p.Key == key return p.Key() == key
}) })
if len(out) == 0 { if len(out) == 0 {
return nil return ipn.LoginProfileView{}
} }
if len(out) > 1 { if len(out) > 1 {
pm.logf("[unexpected] multiple profiles with the same key") pm.logf("[unexpected] multiple profiles with the same key")
@ -194,8 +194,8 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
return nil return nil
} }
if pm.currentProfile.Key != "" && 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)
} }
@ -229,29 +229,36 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
existing = existing[1:] existing = existing[1:]
for _, p := range existing { for _, p := range existing {
// Clear the state. // Clear the state.
if err := pm.store.WriteState(p.Key, nil); err != nil { if err := pm.store.WriteState(p.Key(), nil); err != nil {
// 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 // Remove the profile, knownProfiles will be persisted
// in [profileManager.setProfilePrefs] below. // in [profileManager.setProfilePrefs] below.
delete(pm.knownProfiles, p.ID) delete(pm.knownProfiles, p.ID())
} }
} }
pm.currentProfile = cp pm.currentProfile = cp
if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil { cp, err := pm.setProfilePrefs(nil, prefsIn, np)
if err != nil {
return err return err
} }
return pm.setProfileAsUserDefault(cp) return pm.setProfileAsUserDefault(cp)
} }
// SetProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile] // setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile],
// which is not necessarily the [profileManager.CurrentProfile]. It returns an [errProfileAccessDenied] // returning a read-only view of the updated profile on success. If the specified profile is nil,
// if the specified profile is not accessible by the current user. // it defaults to the current profile. If the profile is not accessible by the current user,
func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) error { // the method returns an [errProfileAccessDenied].
if err := pm.checkProfileAccess(lp); err != nil { func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) (ipn.LoginProfileView, error) {
return err isCurrentProfile := lp == nil || (lp.ID != "" && lp.ID == pm.currentProfile.ID())
if isCurrentProfile {
lp = pm.CurrentProfile().AsStruct()
}
if err := pm.checkProfileAccess(lp.View()); err != nil {
return ipn.LoginProfileView{}, err
} }
// An empty profile.ID indicates that the profile is new, the node info wasn't available, // An empty profile.ID indicates that the profile is new, the node info wasn't available,
@ -291,23 +298,29 @@ func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
lp.UserProfile = up lp.UserProfile = up
lp.NetworkProfile = np lp.NetworkProfile = np
// Update the current profile view to reflect the changes
// if the specified profile is the current profile.
if isCurrentProfile {
pm.currentProfile = lp.View()
}
// An empty profile.ID indicates that the node info is not available yet, // An empty profile.ID indicates that the node info is not available yet,
// and the profile doesn't need to be saved on disk. // and the profile doesn't need to be saved on disk.
if lp.ID != "" { if lp.ID != "" {
pm.knownProfiles[lp.ID] = lp pm.knownProfiles[lp.ID] = lp.View()
if err := pm.writeKnownProfiles(); err != nil { if err := pm.writeKnownProfiles(); err != nil {
return err return ipn.LoginProfileView{}, err
} }
// Clone prefsIn and create a read-only view as a safety measure to // Clone prefsIn and create a read-only view as a safety measure to
// prevent accidental preference mutations, both externally and internally. // prevent accidental preference mutations, both externally and internally.
if err := pm.setProfilePrefsNoPermCheck(lp, prefsIn.AsStruct().View()); err != nil { if err := pm.setProfilePrefsNoPermCheck(lp.View(), prefsIn.AsStruct().View()); err != nil {
return err return ipn.LoginProfileView{}, err
} }
} }
return nil return lp.View(), nil
} }
func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.ProfileID, ipn.StateKey) { func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.ProfileID, ipn.StateKey) {
var idb [2]byte var idb [2]byte
for { for {
rand.Read(idb[:]) rand.Read(idb[:])
@ -326,14 +339,14 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
// The method does not perform any additional checks on the specified // The method does not perform any additional checks on the specified
// profile, such as verifying the caller's access rights or checking // profile, such as verifying the caller's access rights or checking
// if another profile for the same node already exists. // if another profile for the same node already exists.
func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error { func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error {
isCurrentProfile := pm.currentProfile == profile isCurrentProfile := pm.currentProfile == profile
if isCurrentProfile { if isCurrentProfile {
pm.prefs = clonedPrefs pm.prefs = clonedPrefs
pm.updateHealth() pm.updateHealth()
} }
if profile.Key != "" { if profile.Key() != "" {
if err := pm.writePrefsToStore(profile.Key, clonedPrefs); err != nil { if err := pm.writePrefsToStore(profile.Key(), clonedPrefs); err != nil {
return err return err
} }
} else if !isCurrentProfile { } else if !isCurrentProfile {
@ -362,11 +375,11 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
} }
// Profiles returns the list of known profiles accessible to the current user. // Profiles returns the list of known profiles accessible to the current user.
func (pm *profileManager) Profiles() []ipn.LoginProfile { func (pm *profileManager) Profiles() []ipn.LoginProfileView {
allProfiles := pm.allProfiles() allProfiles := pm.allProfiles()
out := make([]ipn.LoginProfile, len(allProfiles)) out := make([]ipn.LoginProfileView, len(allProfiles))
for i, p := range allProfiles { for i, p := range allProfiles {
out[i] = *p out[i] = p
} }
return out return out
} }
@ -374,26 +387,26 @@ func (pm *profileManager) Profiles() []ipn.LoginProfile {
// ProfileByID returns a profile with the given id, if it is accessible to the current user. // 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 exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
// If the profile does not exist, it returns an [errProfileNotFound]. // If the profile does not exist, it returns an [errProfileNotFound].
func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfile, error) { func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfileView, error) {
kp, err := pm.profileByIDNoPermCheck(id) kp, err := pm.profileByIDNoPermCheck(id)
if err != nil { if err != nil {
return ipn.LoginProfile{}, err return ipn.LoginProfileView{}, err
} }
if err := pm.checkProfileAccess(kp); err != nil { if err := pm.checkProfileAccess(kp); err != nil {
return ipn.LoginProfile{}, err return ipn.LoginProfileView{}, err
} }
return *kp, nil return kp, nil
} }
// profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't // profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't
// check user's access rights to the profile. // check user's access rights to the profile.
func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (*ipn.LoginProfile, error) { func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (ipn.LoginProfileView, error) {
if id == pm.currentProfile.ID { if id == pm.currentProfile.ID() {
return pm.currentProfile, nil return pm.currentProfile, nil
} }
kp, ok := pm.knownProfiles[id] kp, ok := pm.knownProfiles[id]
if !ok { if !ok {
return nil, errProfileNotFound return ipn.LoginProfileView{}, errProfileNotFound
} }
return kp, nil return kp, nil
} }
@ -412,11 +425,11 @@ func (pm *profileManager) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error)
return pm.profilePrefs(kp) return pm.profilePrefs(kp)
} }
func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, error) { func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, error) {
if p.ID == pm.currentProfile.ID { if p.ID() == pm.currentProfile.ID() {
return pm.prefs, nil return pm.prefs, nil
} }
return pm.loadSavedPrefs(p.Key) return pm.loadSavedPrefs(p.Key())
} }
// SwitchProfile switches to the profile with the given id. // SwitchProfile switches to the profile with the given id.
@ -429,14 +442,14 @@ 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.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() {
return nil return nil
} }
if err := pm.checkProfileAccess(kp); err != nil { if err := pm.checkProfileAccess(kp); err != nil {
return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id) 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 {
return err return err
} }
@ -459,8 +472,8 @@ func (pm *profileManager) SwitchToDefaultProfile() error {
// setProfileAsUserDefault sets the specified profile as the default for the current user. // 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. // It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user.
func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) error { func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView) error {
if profile.Key == "" { if profile.Key() == "" {
// The profile has not been persisted yet; ignore it for now. // The profile has not been persisted yet; ignore it for now.
return nil return nil
} }
@ -468,7 +481,7 @@ func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) err
return errProfileAccessDenied return errProfileAccessDenied
} }
k := ipn.CurrentProfileKey(string(pm.currentUserID)) k := ipn.CurrentProfileKey(string(pm.currentUserID))
return pm.WriteState(k, []byte(profile.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) {
@ -507,10 +520,10 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
return savedPrefs.View(), nil return savedPrefs.View(), nil
} }
// CurrentProfile returns the current LoginProfile. // CurrentProfile returns a read-only [ipn.LoginProfileView] of the current profile.
// The value may be zero if the profile is not persisted. // The value may be zero if the profile is not persisted.
func (pm *profileManager) CurrentProfile() ipn.LoginProfile { func (pm *profileManager) CurrentProfile() ipn.LoginProfileView {
return *pm.currentProfile return pm.currentProfile
} }
// errProfileNotFound is returned by methods that accept a ProfileID // errProfileNotFound is returned by methods that accept a ProfileID
@ -533,7 +546,7 @@ var errProfileAccessDenied = errors.New("profile access denied")
// recommended to call [profileManager.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 == pm.currentProfile.ID() {
return pm.deleteCurrentProfile() return pm.deleteCurrentProfile()
} }
kp, ok := pm.knownProfiles[id] kp, ok := pm.knownProfiles[id]
@ -550,7 +563,7 @@ func (pm *profileManager) deleteCurrentProfile() error {
if err := pm.checkProfileAccess(pm.currentProfile); err != nil { if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
return err return err
} }
if pm.currentProfile.ID == "" { if pm.currentProfile.ID() == "" {
// Deleting the in-memory only new profile, just create a new one. // Deleting the in-memory only new profile, just create a new one.
pm.NewProfile() pm.NewProfile()
return nil return nil
@ -560,14 +573,14 @@ func (pm *profileManager) deleteCurrentProfile() error {
// deleteProfileNoPermCheck is like [profileManager.DeleteProfile], // deleteProfileNoPermCheck is like [profileManager.DeleteProfile],
// but it doesn't check user's access rights to the profile. // but it doesn't check user's access rights to the profile.
func (pm *profileManager) deleteProfileNoPermCheck(profile *ipn.LoginProfile) error { func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
if profile.ID == pm.currentProfile.ID { if profile.ID() == pm.currentProfile.ID() {
pm.NewProfile() pm.NewProfile()
} }
if err := pm.WriteState(profile.Key, nil); err != nil { if err := pm.WriteState(profile.Key(), nil); err != nil {
return err return err
} }
delete(pm.knownProfiles, profile.ID) delete(pm.knownProfiles, profile.ID())
return pm.writeKnownProfiles() return pm.writeKnownProfiles()
} }
@ -578,7 +591,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
currentProfileDeleted := false currentProfileDeleted := false
writeKnownProfiles := func() error { writeKnownProfiles := func() error {
if currentProfileDeleted || pm.currentProfile.ID == "" { if currentProfileDeleted || pm.currentProfile.ID() == "" {
pm.NewProfile() pm.NewProfile()
} }
return pm.writeKnownProfiles() return pm.writeKnownProfiles()
@ -589,14 +602,14 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
// Skip profiles we don't have access to. // Skip profiles we don't have access to.
continue 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.
writeKnownProfiles() writeKnownProfiles()
return err return err
} }
delete(pm.knownProfiles, kp.ID) delete(pm.knownProfiles, kp.ID())
if kp.ID == pm.currentProfile.ID { if kp.ID() == pm.currentProfile.ID() {
currentProfileDeleted = true currentProfileDeleted = true
} }
} }
@ -633,26 +646,27 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
pm.prefs = defaultPrefs pm.prefs = defaultPrefs
pm.updateHealth() pm.updateHealth()
pm.currentProfile = &ipn.LoginProfile{LocalUserID: uid} newProfile := &ipn.LoginProfile{LocalUserID: uid}
pm.currentProfile = newProfile.View()
} }
// newProfileWithPrefs creates a new profile with the specified prefs and assigns // newProfileWithPrefs creates a new profile with the specified prefs and assigns
// the specified uid as the profile owner. If switchNow is true, it switches to the // 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, // newly created profile immediately. It returns the newly created profile on success,
// or an error on failure. // or an error on failure.
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) { func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) {
metricNewProfile.Add(1) metricNewProfile.Add(1)
profile := &ipn.LoginProfile{LocalUserID: uid} profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
if err := pm.SetProfilePrefs(profile, prefs, ipn.NetworkProfile{}); err != nil { if err != nil {
return nil, err return ipn.LoginProfileView{}, err
} }
if switchNow { if switchNow {
pm.currentProfile = profile pm.currentProfile = profile
pm.prefs = prefs.AsStruct().View() pm.prefs = prefs.AsStruct().View()
pm.updateHealth() pm.updateHealth()
if err := pm.setProfileAsUserDefault(profile); err != nil { if err := pm.setProfileAsUserDefault(profile); err != nil {
return nil, err return ipn.LoginProfileView{}, err
} }
} }
return profile, nil return profile, nil
@ -711,8 +725,8 @@ func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
return ipn.StateKey(autoStartKey), nil return ipn.StateKey(autoStartKey), nil
} }
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) { func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]ipn.LoginProfileView, error) {
var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile var knownProfiles map[ipn.ProfileID]ipn.LoginProfileView
prfB, err := store.ReadState(ipn.KnownProfilesStateKey) prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
switch err { switch err {
case nil: case nil:
@ -720,7 +734,7 @@ func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfil
return nil, fmt.Errorf("unmarshaling known profiles: %w", err) return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
} }
case ipn.ErrStateNotExist: case ipn.ErrStateNotExist:
knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile) knownProfiles = make(map[ipn.ProfileID]ipn.LoginProfileView)
default: default:
return nil, fmt.Errorf("calling ReadState on state store: %w", err) return nil, fmt.Errorf("calling ReadState on state store: %w", err)
} }
@ -749,17 +763,17 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
if stateKey != "" { if stateKey != "" {
for _, v := range knownProfiles { for _, v := range knownProfiles {
if v.Key == stateKey { if v.Key() == stateKey {
pm.currentProfile = v pm.currentProfile = v
} }
} }
if pm.currentProfile == nil { if !pm.currentProfile.Valid() {
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok { if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
pm.currentUserID = ipn.WindowsUserID(suf) pm.currentUserID = ipn.WindowsUserID(suf)
} }
pm.NewProfile() pm.NewProfile()
} else { } else {
pm.currentUserID = pm.currentProfile.LocalUserID pm.currentUserID = pm.currentProfile.LocalUserID()
} }
prefs, err := pm.loadSavedPrefs(stateKey) prefs, err := pm.loadSavedPrefs(stateKey)
if err != nil { if err != nil {
@ -788,18 +802,18 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
return pm, nil return pm, nil
} }
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (*ipn.LoginProfile, error) { func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (ipn.LoginProfileView, error) {
metricMigration.Add(1) metricMigration.Add(1)
sentinel, prefs, err := pm.loadLegacyPrefs(uid) sentinel, prefs, err := pm.loadLegacyPrefs(uid)
if err != nil { if err != nil {
metricMigrationError.Add(1) metricMigrationError.Add(1)
return nil, fmt.Errorf("load legacy prefs: %w", err) return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
} }
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel) pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow) profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
if err != nil { if err != nil {
metricMigrationError.Add(1) metricMigrationError.Add(1)
return nil, fmt.Errorf("migrating _daemon profile: %w", err) return ipn.LoginProfileView{}, 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)
@ -809,8 +823,8 @@ func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNo
func (pm *profileManager) requiresBackfill() bool { func (pm *profileManager) requiresBackfill() bool {
return pm != nil && return pm != nil &&
pm.currentProfile != nil && pm.currentProfile.Valid() &&
pm.currentProfile.NetworkProfile.RequiresBackfill() pm.currentProfile.NetworkProfile().RequiresBackfill()
} }
var ( var (

View File

@ -52,11 +52,11 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
pm.SetCurrentUserID("user1") pm.SetCurrentUserID("user1")
newProfile(t, "user1") newProfile(t, "user1")
cp := pm.currentProfile cp := pm.currentProfile
pm.DeleteProfile(cp.ID) pm.DeleteProfile(cp.ID())
if pm.currentProfile == nil { if !pm.currentProfile.Valid() {
t.Fatal("currentProfile is nil") t.Fatal("currentProfile is nil")
} else if pm.currentProfile.ID != "" { } else if pm.currentProfile.ID() != "" {
t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID) t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID())
} }
if !pm.CurrentPrefs().Equals(defaultPrefs) { if !pm.CurrentPrefs().Equals(defaultPrefs) {
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty()) t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
@ -67,10 +67,10 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
pm.SetCurrentUserID("user1") pm.SetCurrentUserID("user1")
if pm.currentProfile == nil { if !pm.currentProfile.Valid() {
t.Fatal("currentProfile is nil") t.Fatal("currentProfile is nil")
} else if pm.currentProfile.ID != "" { } else if pm.currentProfile.ID() != "" {
t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID) t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID())
} }
if !pm.CurrentPrefs().Equals(defaultPrefs) { if !pm.CurrentPrefs().Equals(defaultPrefs) {
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty()) t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
@ -110,8 +110,8 @@ func TestProfileList(t *testing.T) {
t.Fatalf("got %d profiles, want %d", len(got), len(want)) t.Fatalf("got %d profiles, want %d", len(got), len(want))
} }
for i, w := range want { for i, w := range want {
if got[i].Name != w { if got[i].Name() != w {
t.Errorf("got profile %d name %q, want %q", i, got[i].Name, w) t.Errorf("got profile %d name %q, want %q", i, got[i].Name(), w)
} }
} }
} }
@ -129,10 +129,10 @@ func TestProfileList(t *testing.T) {
pm.SetCurrentUserID("user1") pm.SetCurrentUserID("user1")
checkProfiles(t, "alice", "bob") checkProfiles(t, "alice", "bob")
if lp := pm.findProfileByKey(carol.Key); lp != nil { if lp := pm.findProfileByKey(carol.Key()); lp.Valid() {
t.Fatalf("found profile for user2 in user1's profile list") t.Fatalf("found profile for user2 in user1's profile list")
} }
if lp := pm.findProfileByName(carol.Name); lp != nil { if lp := pm.findProfileByName(carol.Name()); lp.Valid() {
t.Fatalf("found profile for user2 in user1's profile list") t.Fatalf("found profile for user2 in user1's profile list")
} }
@ -294,7 +294,7 @@ func TestProfileDupe(t *testing.T) {
profs := pm.Profiles() profs := pm.Profiles()
var got []*persist.Persist var got []*persist.Persist
for _, p := range profs { for _, p := range profs {
prefs, err := pm.loadSavedPrefs(p.Key) prefs, err := pm.loadSavedPrefs(p.Key())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -328,9 +328,9 @@ func TestProfileManagement(t *testing.T) {
checkProfiles := func(t *testing.T) { checkProfiles := func(t *testing.T) {
t.Helper() t.Helper()
prof := pm.CurrentProfile() prof := pm.CurrentProfile()
t.Logf("\tCurrentProfile = %q", prof) t.Logf("\tCurrentProfile = %q", prof.Name())
if prof.Name != wantCurProfile { if prof.Name() != wantCurProfile {
t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile)
} }
profiles := pm.Profiles() profiles := pm.Profiles()
wantLen := len(wantProfiles) wantLen := len(wantProfiles)
@ -349,13 +349,13 @@ func TestProfileManagement(t *testing.T) {
t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
} }
for _, p := range profiles { for _, p := range profiles {
got, err := pm.loadSavedPrefs(p.Key) got, err := pm.loadSavedPrefs(p.Key())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Use Hostname as a proxy for all prefs. // Use Hostname as a proxy for all prefs.
if !got.Equals(wantProfiles[p.Name]) { if !got.Equals(wantProfiles[p.Name()]) {
t.Fatalf("Prefs for profile %q =\n got=%+v\nwant=%v", p, got.Pretty(), wantProfiles[p.Name].Pretty()) t.Fatalf("Prefs for profile %q =\n got=%+v\nwant=%v", p.Name(), got.Pretty(), wantProfiles[p.Name()].Pretty())
} }
} }
} }
@ -422,7 +422,7 @@ func TestProfileManagement(t *testing.T) {
checkProfiles(t) checkProfiles(t)
t.Logf("Delete default profile") t.Logf("Delete default profile")
if err := pm.DeleteProfile(pm.findProfileByName("user@1.example.com").ID); err != nil { if err := pm.DeleteProfile(pm.ProfileIDForName("user@1.example.com")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
delete(wantProfiles, "user@1.example.com") delete(wantProfiles, "user@1.example.com")
@ -506,9 +506,9 @@ func TestProfileManagementWindows(t *testing.T) {
checkProfiles := func(t *testing.T) { checkProfiles := func(t *testing.T) {
t.Helper() t.Helper()
prof := pm.CurrentProfile() prof := pm.CurrentProfile()
t.Logf("\tCurrentProfile = %q", prof) t.Logf("\tCurrentProfile = %q", prof.Name())
if prof.Name != wantCurProfile { if prof.Name() != wantCurProfile {
t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile)
} }
if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) { if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) {
t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())

View File

@ -318,7 +318,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
bs = j bs = j
} }
profileID := b.pm.CurrentProfile().ID profileID := b.pm.CurrentProfile().ID()
confKey := ipn.ServeConfigKey(profileID) confKey := ipn.ServeConfigKey(profileID)
if err := b.store.WriteState(confKey, bs); err != nil { if err := b.store.WriteState(confKey, bs); err != nil {
return fmt.Errorf("writing ServeConfig to StateStore: %w", err) return fmt.Errorf("writing ServeConfig to StateStore: %w", err)

View File

@ -898,7 +898,7 @@ func newTestBackend(t *testing.T) *LocalBackend {
b.SetVarRoot(dir) b.SetVarRoot(dir)
pm := must.Get(newProfileManager(new(mem.Store), logf, new(health.Tracker))) pm := must.Get(newProfileManager(new(mem.Store), logf, new(health.Tracker)))
pm.currentProfile = &ipn.LoginProfile{ID: "id0"} pm.currentProfile = (&ipn.LoginProfile{ID: "id0"}).View()
b.pm = pm b.pm = pm
b.netMap = &netmap.NetworkMap{ b.netMap = &netmap.NetworkMap{

View File

@ -2601,8 +2601,8 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case httpm.GET: case httpm.GET:
profiles := h.b.ListProfiles() profiles := h.b.ListProfiles()
profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfile) bool { profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfileView) bool {
return p.ID == profileID return p.ID() == profileID
}) })
if profileIndex == -1 { if profileIndex == -1 {
http.Error(w, "Profile not found", http.StatusNotFound) http.Error(w, "Profile not found", http.StatusNotFound)