diff --git a/ipn/doc.go b/ipn/doc.go index 9a0bbb800..c98c7e8b3 100644 --- a/ipn/doc.go +++ b/ipn/doc.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // 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 // control plane and the local network stack. diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 47cca71d0..4050fec46 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -17,6 +17,29 @@ "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. // The result aliases no memory with the original. func (src *Prefs) Clone() *Prefs { diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 41b4ddbc8..e633a2633 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -18,7 +18,73 @@ "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. func (p *Prefs) View() PrefsView { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index a6e3f1952..5766365b1 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -4045,7 +4045,7 @@ func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error { // No profile with that name exists. That's fine. return nil } - if id != b.pm.CurrentProfile().ID { + if id != b.pm.CurrentProfile().ID() { // Name is already in use by another profile. 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() - np := b.pm.CurrentProfile().NetworkProfile + np := b.pm.CurrentProfile().NetworkProfile() if netMap != nil { np = ipn.NetworkProfile{ MagicDNSName: b.netMap.MagicDNSSuffix(), @@ -5663,7 +5663,7 @@ func (b *LocalBackend) Logout(ctx context.Context) error { unlock = b.lockAndGetUnlock() 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) 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 // foreground sessions are not removed if they are not saved on disk. 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. // Don't try to load the serve config. b.lastServeConfJSON = mem.B(nil) @@ -6047,7 +6047,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { return } - confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID) + confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID()) // TODO(maisem,bradfitz): prevent reading the config from disk // if the profile has not changed. 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. // If the profile is not known, it returns an errProfileNotFound. func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error { - if b.CurrentProfile().ID == profile { + if b.CurrentProfile().ID() == profile { return nil } unlock := b.lockAndGetUnlock() @@ -7023,12 +7023,12 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error { func (b *LocalBackend) initTKALocked() error { cp := b.pm.CurrentProfile() - if cp.ID == "" { + if cp.ID() == "" { b.tka = nil return nil } if b.tka != nil { - if b.tka.profile == cp.ID { + if b.tka.profile == cp.ID() { // Already initialized. return nil } @@ -7058,7 +7058,7 @@ func (b *LocalBackend) initTKALocked() error { } b.tka = &tkaState{ - profile: cp.ID, + profile: cp.ID(), authority: authority, storage: storage, } @@ -7111,7 +7111,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error { unlock := b.lockAndGetUnlock() defer unlock() - needToRestart := b.pm.CurrentProfile().ID == p + needToRestart := b.pm.CurrentProfile().ID() == p if err := b.pm.DeleteProfile(p); err != nil { if err == errProfileNotFound { return nil @@ -7126,7 +7126,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error { // CurrentProfile returns the current LoginProfile. // 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() defer b.mu.Unlock() return b.pm.CurrentProfile() @@ -7147,7 +7147,7 @@ func (b *LocalBackend) NewProfile() error { } // ListProfiles returns a list of all LoginProfiles. -func (b *LocalBackend) ListProfiles() []ipn.LoginProfile { +func (b *LocalBackend) ListProfiles() []ipn.LoginProfileView { b.mu.Lock() defer b.mu.Unlock() 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 func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.StateKey { - return pm.CurrentProfile().Key + "||" + key + return pm.CurrentProfile().Key() + "||" + key } const routeInfoStateStoreKey ipn.StateKey = "_routeInfo" @@ -7361,7 +7361,7 @@ func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.Sta func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error { b.mu.Lock() defer b.mu.Unlock() - if b.pm.CurrentProfile().ID == "" { + if b.pm.CurrentProfile().ID() == "" { return nil } key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey) @@ -7373,7 +7373,7 @@ func (b *LocalBackend) storeRouteInfo(ri *appc.RouteInfo) error { } func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) { - if b.pm.CurrentProfile().ID == "" { + if b.pm.CurrentProfile().ID() == "" { return &appc.RouteInfo{}, nil } key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey) diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index de9ebf9fb..3455cab1f 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -4087,9 +4087,9 @@ func TestReadWriteRouteInfo(t *testing.T) { b := newTestBackend(t) prof1 := ipn.LoginProfile{ID: "id1", Key: "key1"} prof2 := ipn.LoginProfile{ID: "id2", Key: "key2"} - b.pm.knownProfiles["id1"] = &prof1 - b.pm.knownProfiles["id2"] = &prof2 - b.pm.currentProfile = &prof1 + b.pm.knownProfiles["id1"] = prof1.View() + b.pm.knownProfiles["id2"] = prof2.View() + b.pm.currentProfile = prof1.View() // set up routeInfo ri1 := &appc.RouteInfo{} diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index bf14d339e..e1583dab7 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -407,7 +407,7 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error { // // b.mu must be held. 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 @@ -455,7 +455,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per } b.tka = &tkaState{ - profile: b.pm.CurrentProfile().ID, + profile: b.pm.CurrentProfile().ID(), authority: authority, storage: chonk, } diff --git a/ipn/ipnlocal/network-lock_test.go b/ipn/ipnlocal/network-lock_test.go index 4b79136c8..838f16cb9 100644 --- a/ipn/ipnlocal/network-lock_test.go +++ b/ipn/ipnlocal/network-lock_test.go @@ -202,7 +202,7 @@ func TestTKADisablementFlow(t *testing.T) { }).View(), ipn.NetworkProfile{})) 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) chonk, err := tka.ChonkDir(tkaPath) if err != nil { @@ -410,7 +410,7 @@ type tkaSyncScenario struct { } 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) // Setup the TKA authority on the node. nodeStorage, err := tka.ChonkDir(tkaPath) @@ -710,7 +710,7 @@ func TestTKADisable(t *testing.T) { }).View(), ipn.NetworkProfile{})) 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) key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} chonk, err := tka.ChonkDir(tkaPath) @@ -770,7 +770,7 @@ func TestTKADisable(t *testing.T) { ccAuto: cc, logf: t.Logf, tka: &tkaState{ - profile: pm.CurrentProfile().ID, + profile: pm.CurrentProfile().ID(), authority: authority, storage: chonk, }, @@ -805,7 +805,7 @@ func TestTKASign(t *testing.T) { key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} 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) chonk, err := tka.ChonkDir(tkaPath) if err != nil { @@ -890,7 +890,7 @@ func TestTKAForceDisable(t *testing.T) { }).View(), ipn.NetworkProfile{})) 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) chonk, err := tka.ChonkDir(tkaPath) if err != nil { @@ -989,7 +989,7 @@ func TestTKAAffectedSigs(t *testing.T) { tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} 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) chonk, err := tka.ChonkDir(tkaPath) if err != nil { @@ -1124,7 +1124,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) { compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1} 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) chonk, err := tka.ChonkDir(tkaPath) if err != nil { diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index b13f921d6..858623025 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -35,9 +35,9 @@ type profileManager struct { health *health.Tracker currentUserID ipn.WindowsUserID - knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil - currentProfile *ipn.LoginProfile // always non-nil - prefs ipn.PrefsView // always Valid. + knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil + currentProfile ipn.LoginProfileView // always Valid. + prefs ipn.PrefsView // always Valid. } func (pm *profileManager) dlogf(format string, args ...any) { @@ -89,7 +89,7 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences") profile, err := pm.migrateFromLegacyPrefs(uid, false) if err == nil { - return profile.ID + return profile.ID() } 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)) prof := pm.findProfileByKey(pk) - if prof == nil { + if !prof.Valid() { pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk) return "" } - return prof.ID + return prof.ID() } // checkProfileAccess returns an [errProfileAccessDenied] if the current user // does not have access to the specified profile. -func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error { - if pm.currentUserID != "" && profile.LocalUserID != pm.currentUserID { +func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error { + if pm.currentUserID != "" && profile.LocalUserID() != pm.currentUserID { return errProfileAccessDenied } return nil @@ -116,21 +116,21 @@ func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error { // allProfiles returns all profiles accessible to the current user. // 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 { if pm.checkProfileAccess(p) == nil { out = append(out, p) } } - slices.SortFunc(out, func(a, b *ipn.LoginProfile) int { - return cmp.Compare(a.Name, b.Name) + slices.SortFunc(out, func(a, b ipn.LoginProfileView) int { + return cmp.Compare(a.Name(), b.Name()) }) return out } // matchingProfiles is like [profileManager.allProfiles], but returns only profiles // matching the given predicate. -func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) { +func (pm *profileManager) matchingProfiles(f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) { all := pm.allProfiles() out = all[:0] 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 // that represent the same node/user as prefs. // The returned profiles are sorted by Name. -func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.LoginProfile { - return pm.matchingProfiles(func(p *ipn.LoginProfile) bool { - return p.ControlURL == prefs.ControlURL() && - (p.UserProfile.ID == prefs.Persist().UserProfile().ID || - p.NodeID == prefs.Persist().NodeID()) +func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []ipn.LoginProfileView { + return pm.matchingProfiles(func(p ipn.LoginProfileView) bool { + return p.ControlURL() == prefs.ControlURL() && + (p.UserProfile().ID == prefs.Persist().UserProfile().ID || + p.NodeID() == prefs.Persist().NodeID()) }) } @@ -157,18 +157,18 @@ func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.Login // accessible to the current user. func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID { p := pm.findProfileByName(name) - if p == nil { + if !p.Valid() { return "" } - return p.ID + return p.ID() } -func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile { - out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool { - return p.Name == name +func (pm *profileManager) findProfileByName(name string) ipn.LoginProfileView { + out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool { + return p.Name() == name }) if len(out) == 0 { - return nil + return ipn.LoginProfileView{} } if len(out) > 1 { pm.logf("[unexpected] multiple profiles with the same name") @@ -176,12 +176,12 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile { return out[0] } -func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile { - out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool { - return p.Key == key +func (pm *profileManager) findProfileByKey(key ipn.StateKey) ipn.LoginProfileView { + out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool { + return p.Key() == key }) if len(out) == 0 { - return nil + return ipn.LoginProfileView{} } if len(out) > 1 { pm.logf("[unexpected] multiple profiles with the same key") @@ -194,8 +194,8 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error { return nil } - if pm.currentProfile.Key != "" && pm.prefs.ForceDaemon() { - return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key)) + if pm.currentProfile.Key() != "" && pm.prefs.ForceDaemon() { + return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key())) } else { return pm.WriteState(ipn.ServerModeStartKey, nil) } @@ -229,29 +229,36 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) existing = existing[1:] for _, p := range existing { // 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. continue } // Remove the profile, knownProfiles will be persisted // in [profileManager.setProfilePrefs] below. - delete(pm.knownProfiles, p.ID) + delete(pm.knownProfiles, p.ID()) } } 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 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 +// setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile], +// returning a read-only view of the updated profile on success. If the specified profile is nil, +// it defaults to the current profile. If the profile is not accessible by the current user, +// the method returns an [errProfileAccessDenied]. +func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) (ipn.LoginProfileView, error) { + 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, @@ -291,23 +298,29 @@ func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref lp.UserProfile = up 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, // and the profile doesn't need to be saved on disk. if lp.ID != "" { - pm.knownProfiles[lp.ID] = lp + pm.knownProfiles[lp.ID] = lp.View() 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 // prevent accidental preference mutations, both externally and internally. - if err := pm.setProfilePrefsNoPermCheck(lp, prefsIn.AsStruct().View()); err != nil { - return err + if err := pm.setProfilePrefsNoPermCheck(lp.View(), prefsIn.AsStruct().View()); err != nil { + 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 for { 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 // profile, such as verifying the caller's access rights or checking // if another profile for the same node already exists. -func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error { +func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error { isCurrentProfile := pm.currentProfile == profile if isCurrentProfile { pm.prefs = clonedPrefs pm.updateHealth() } - if profile.Key != "" { - if err := pm.writePrefsToStore(profile.Key, clonedPrefs); err != nil { + if profile.Key() != "" { + if err := pm.writePrefsToStore(profile.Key(), clonedPrefs); err != nil { return err } } 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. -func (pm *profileManager) Profiles() []ipn.LoginProfile { +func (pm *profileManager) Profiles() []ipn.LoginProfileView { allProfiles := pm.allProfiles() - out := make([]ipn.LoginProfile, len(allProfiles)) + out := make([]ipn.LoginProfileView, len(allProfiles)) for i, p := range allProfiles { - out[i] = *p + out[i] = p } 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. // 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) { +func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfileView, error) { kp, err := pm.profileByIDNoPermCheck(id) if err != nil { - return ipn.LoginProfile{}, err + return ipn.LoginProfileView{}, err } 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 // check user's access rights to the profile. -func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (*ipn.LoginProfile, error) { - if id == pm.currentProfile.ID { +func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (ipn.LoginProfileView, error) { + if id == pm.currentProfile.ID() { return pm.currentProfile, nil } kp, ok := pm.knownProfiles[id] if !ok { - return nil, errProfileNotFound + return ipn.LoginProfileView{}, errProfileNotFound } return kp, nil } @@ -412,11 +425,11 @@ func (pm *profileManager) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error) return pm.profilePrefs(kp) } -func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, error) { - if p.ID == pm.currentProfile.ID { +func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, error) { + if p.ID() == pm.currentProfile.ID() { return pm.prefs, nil } - return pm.loadSavedPrefs(p.Key) + return pm.loadSavedPrefs(p.Key()) } // SwitchProfile switches to the profile with the given id. @@ -429,14 +442,14 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error { if !ok { 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 } 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 { return err } @@ -459,8 +472,8 @@ func (pm *profileManager) SwitchToDefaultProfile() error { // 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 == "" { +func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView) error { + if profile.Key() == "" { // The profile has not been persisted yet; ignore it for now. return nil } @@ -468,7 +481,7 @@ func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) err return errProfileAccessDenied } 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) { @@ -507,10 +520,10 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error 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. -func (pm *profileManager) CurrentProfile() ipn.LoginProfile { - return *pm.currentProfile +func (pm *profileManager) CurrentProfile() ipn.LoginProfileView { + return pm.currentProfile } // errProfileNotFound is returned by methods that accept a ProfileID @@ -533,7 +546,7 @@ func (pm *profileManager) CurrentProfile() ipn.LoginProfile { // recommended to call [profileManager.SwitchProfile] first. func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error { metricDeleteProfile.Add(1) - if id == pm.currentProfile.ID { + if id == pm.currentProfile.ID() { return pm.deleteCurrentProfile() } kp, ok := pm.knownProfiles[id] @@ -550,7 +563,7 @@ func (pm *profileManager) deleteCurrentProfile() error { if err := pm.checkProfileAccess(pm.currentProfile); err != nil { return err } - if pm.currentProfile.ID == "" { + if pm.currentProfile.ID() == "" { // Deleting the in-memory only new profile, just create a new one. pm.NewProfile() return nil @@ -560,14 +573,14 @@ func (pm *profileManager) deleteCurrentProfile() error { // 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 { +func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error { + if profile.ID() == pm.currentProfile.ID() { pm.NewProfile() } - if err := pm.WriteState(profile.Key, nil); err != nil { + if err := pm.WriteState(profile.Key(), nil); err != nil { return err } - delete(pm.knownProfiles, profile.ID) + delete(pm.knownProfiles, profile.ID()) return pm.writeKnownProfiles() } @@ -578,7 +591,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error { currentProfileDeleted := false writeKnownProfiles := func() error { - if currentProfileDeleted || pm.currentProfile.ID == "" { + if currentProfileDeleted || pm.currentProfile.ID() == "" { pm.NewProfile() } return pm.writeKnownProfiles() @@ -589,14 +602,14 @@ func (pm *profileManager) DeleteAllProfilesForUser() error { // 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 // return the original error. writeKnownProfiles() return err } - delete(pm.knownProfiles, kp.ID) - if kp.ID == pm.currentProfile.ID { + delete(pm.knownProfiles, kp.ID()) + if kp.ID() == pm.currentProfile.ID() { currentProfileDeleted = true } } @@ -633,26 +646,27 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) { pm.prefs = defaultPrefs 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 // 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) { +func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) { metricNewProfile.Add(1) - profile := &ipn.LoginProfile{LocalUserID: uid} - if err := pm.SetProfilePrefs(profile, prefs, ipn.NetworkProfile{}); err != nil { - return nil, err + profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{}) + if err != nil { + return ipn.LoginProfileView{}, err } if switchNow { pm.currentProfile = profile pm.prefs = prefs.AsStruct().View() pm.updateHealth() if err := pm.setProfileAsUserDefault(profile); err != nil { - return nil, err + return ipn.LoginProfileView{}, err } } return profile, nil @@ -711,8 +725,8 @@ func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) { return ipn.StateKey(autoStartKey), nil } -func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) { - var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile +func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]ipn.LoginProfileView, error) { + var knownProfiles map[ipn.ProfileID]ipn.LoginProfileView prfB, err := store.ReadState(ipn.KnownProfilesStateKey) switch err { 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) } case ipn.ErrStateNotExist: - knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile) + knownProfiles = make(map[ipn.ProfileID]ipn.LoginProfileView) default: 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 != "" { for _, v := range knownProfiles { - if v.Key == stateKey { + if v.Key() == stateKey { pm.currentProfile = v } } - if pm.currentProfile == nil { + if !pm.currentProfile.Valid() { if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok { pm.currentUserID = ipn.WindowsUserID(suf) } pm.NewProfile() } else { - pm.currentUserID = pm.currentProfile.LocalUserID + pm.currentUserID = pm.currentProfile.LocalUserID() } prefs, err := pm.loadSavedPrefs(stateKey) if err != nil { @@ -788,18 +802,18 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt 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) sentinel, prefs, err := pm.loadLegacyPrefs(uid) if err != nil { 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) profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow) if err != nil { 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.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 { return pm != nil && - pm.currentProfile != nil && - pm.currentProfile.NetworkProfile.RequiresBackfill() + pm.currentProfile.Valid() && + pm.currentProfile.NetworkProfile().RequiresBackfill() } var ( diff --git a/ipn/ipnlocal/profiles_test.go b/ipn/ipnlocal/profiles_test.go index 73e4f6535..5c4f1fd4c 100644 --- a/ipn/ipnlocal/profiles_test.go +++ b/ipn/ipnlocal/profiles_test.go @@ -52,11 +52,11 @@ func TestProfileCurrentUserSwitch(t *testing.T) { pm.SetCurrentUserID("user1") newProfile(t, "user1") cp := pm.currentProfile - pm.DeleteProfile(cp.ID) - if pm.currentProfile == nil { + pm.DeleteProfile(cp.ID()) + if !pm.currentProfile.Valid() { t.Fatal("currentProfile is nil") - } else if pm.currentProfile.ID != "" { - t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID) + } else if pm.currentProfile.ID() != "" { + t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID()) } if !pm.CurrentPrefs().Equals(defaultPrefs) { t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty()) @@ -67,10 +67,10 @@ func TestProfileCurrentUserSwitch(t *testing.T) { t.Fatal(err) } pm.SetCurrentUserID("user1") - if pm.currentProfile == nil { + if !pm.currentProfile.Valid() { t.Fatal("currentProfile is nil") - } else if pm.currentProfile.ID != "" { - t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID) + } else if pm.currentProfile.ID() != "" { + t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID()) } if !pm.CurrentPrefs().Equals(defaultPrefs) { 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)) } for i, w := range want { - if got[i].Name != w { - t.Errorf("got profile %d name %q, want %q", i, got[i].Name, w) + if 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") 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") } - 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") } @@ -294,7 +294,7 @@ type step struct { profs := pm.Profiles() var got []*persist.Persist for _, p := range profs { - prefs, err := pm.loadSavedPrefs(p.Key) + prefs, err := pm.loadSavedPrefs(p.Key()) if err != nil { t.Fatal(err) } @@ -328,9 +328,9 @@ func TestProfileManagement(t *testing.T) { checkProfiles := func(t *testing.T) { t.Helper() prof := pm.CurrentProfile() - t.Logf("\tCurrentProfile = %q", prof) - if prof.Name != wantCurProfile { - t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) + t.Logf("\tCurrentProfile = %q", prof.Name()) + if prof.Name() != wantCurProfile { + t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile) } profiles := pm.Profiles() wantLen := len(wantProfiles) @@ -349,13 +349,13 @@ func TestProfileManagement(t *testing.T) { t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) } for _, p := range profiles { - got, err := pm.loadSavedPrefs(p.Key) + got, err := pm.loadSavedPrefs(p.Key()) if err != nil { t.Fatal(err) } // Use Hostname as a proxy for all prefs. - if !got.Equals(wantProfiles[p.Name]) { - t.Fatalf("Prefs for profile %q =\n got=%+v\nwant=%v", p, got.Pretty(), wantProfiles[p.Name].Pretty()) + if !got.Equals(wantProfiles[p.Name()]) { + 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) 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) } delete(wantProfiles, "user@1.example.com") @@ -506,9 +506,9 @@ func TestProfileManagementWindows(t *testing.T) { checkProfiles := func(t *testing.T) { t.Helper() prof := pm.CurrentProfile() - t.Logf("\tCurrentProfile = %q", prof) - if prof.Name != wantCurProfile { - t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) + t.Logf("\tCurrentProfile = %q", prof.Name()) + if prof.Name() != wantCurProfile { + t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile) } if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) { t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 63cb2ef55..638b26a36 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -318,7 +318,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string bs = j } - profileID := b.pm.CurrentProfile().ID + profileID := b.pm.CurrentProfile().ID() confKey := ipn.ServeConfigKey(profileID) if err := b.store.WriteState(confKey, bs); err != nil { return fmt.Errorf("writing ServeConfig to StateStore: %w", err) diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index eb8169390..7f457e560 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -898,7 +898,7 @@ func newTestBackend(t *testing.T) *LocalBackend { b.SetVarRoot(dir) 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.netMap = &netmap.NetworkMap{ diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index e6b537d8f..154d309a1 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -2601,8 +2601,8 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) { switch r.Method { case httpm.GET: profiles := h.b.ListProfiles() - profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfile) bool { - return p.ID == profileID + profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfileView) bool { + return p.ID() == profileID }) if profileIndex == -1 { http.Error(w, "Profile not found", http.StatusNotFound)