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
// 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.

View File

@ -17,6 +17,29 @@ import (
"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 {

View File

@ -18,7 +18,73 @@ import (
"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 {

View File

@ -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 @@ const routeInfoStateStoreKey ipn.StateKey = "_routeInfo"
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)

View File

@ -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{}

View File

@ -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,
}

View File

@ -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 @@ func TestTKASync(t *testing.T) {
}
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 {

View File

@ -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 @@ var errProfileAccessDenied = errors.New("profile access denied")
// 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 (

View File

@ -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 @@ func TestProfileDupe(t *testing.T) {
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())

View File

@ -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)

View File

@ -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{

View File

@ -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)