ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback

In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.

Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.

Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.

Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl
2025-04-14 10:45:08 -05:00
committed by Nick Khyl
parent b926cd7fc6
commit e6eba4efee
6 changed files with 370 additions and 81 deletions

View File

@@ -42,6 +42,19 @@ type profileManager struct {
knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
currentProfile ipn.LoginProfileView // always Valid.
prefs ipn.PrefsView // always Valid.
// extHost is the bridge between [profileManager] and the registered [ipnext.Extension]s.
// It may be nil in tests. A nil pointer is a valid, no-op host.
extHost *ExtensionHost
}
// SetExtensionHost sets the [ExtensionHost] for the [profileManager].
// The specified host will be notified about profile and prefs changes
// and will immediately be notified about the current profile and prefs.
// A nil host is a valid, no-op host.
func (pm *profileManager) SetExtensionHost(host *ExtensionHost) {
pm.extHost = host
host.NotifyProfileChange(pm.currentProfile, pm.prefs, false)
}
func (pm *profileManager) dlogf(format string, args ...any) {
@@ -321,7 +334,6 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
return err
}
return pm.setProfileAsUserDefault(cp)
}
// setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile],
@@ -419,7 +431,27 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.Prof
func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error {
isCurrentProfile := pm.currentProfile == profile
if isCurrentProfile {
oldPrefs := pm.prefs
pm.prefs = clonedPrefs
// Sadly, profile prefs can be changed in multiple ways.
// It's pretty chaotic, and in many cases callers use
// unexported methods of the profile manager instead of
// going through [LocalBackend.setPrefsLockedOnEntry]
// or at least using [profileManager.SetPrefs].
//
// While we should definitely clean this up to improve
// the overall structure of how prefs are set, which would
// also address current and future conflicts, such as
// competing features changing the same prefs, this method
// is currently the central place where we can detect all
// changes to the current profile's prefs.
//
// That said, regardless of the cleanup, we might want
// to keep the profileManager responsible for invoking
// profile- and prefs-related callbacks.
pm.extHost.NotifyProfilePrefsChanged(pm.currentProfile, oldPrefs, clonedPrefs)
pm.updateHealth()
}
if profile.Key() != "" {
@@ -705,6 +737,9 @@ func (pm *profileManager) SwitchToNewProfileForUser(uid ipn.WindowsUserID) {
pm.SwitchToProfile(pm.NewProfileForUser(uid))
}
// zeroProfile is a read-only view of a new, empty profile that is not persisted to the store.
var zeroProfile = (&ipn.LoginProfile{}).View()
// NewProfileForUser creates a new profile for the specified user and returns a read-only view of it.
// It neither switches to the new profile nor persists it to the store.
func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) ipn.LoginProfileView {